From f9cb901fe11ee35c4cfce57bb2eb6d4dac88019d Mon Sep 17 00:00:00 2001 From: Valentyn Berezin Date: Mon, 11 Oct 2021 18:37:28 +0300 Subject: [PATCH] Release 1.0.0 master (#1315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed plugin version * Bump version * Start next iteration 0.31.0-SNAPSHOT update version in docker-compose fix git hooks errors * Bump firefly-exporter version * Bump fasterxml.version from 2.10.1 to 2.12.1 Bumps `fasterxml.version` from 2.10.1 to 2.12.1. Updates `jackson-annotations` from 2.10.1 to 2.12.1 - [Release notes](https://github.com/FasterXML/jackson/releases) - [Commits](https://github.com/FasterXML/jackson/commits) Updates `jackson-databind` from 2.10.1 to 2.12.1 - [Release notes](https://github.com/FasterXML/jackson/releases) - [Commits](https://github.com/FasterXML/jackson/commits) Updates `jackson-datatype-jsr310` from 2.10.1 to 2.12.1 Signed-off-by: dependabot[bot] * update to new postgres container * Forced cleanup * OBG-1184. Fixed expired QWAC cert * #1002 xs2a adapter updated to version 0.1.2 * #1002 fix tests * OBG-1002 xs2a adapter version 0.1.4 * test fixes for new xs2a adapter * OBG-1002 xs2a adapter 0.1.8 * OBG-959 update multibanking to 5.5.24 update lombok and mapstruct to work correctly with last intelliJ https://github.com/rzwitserloot/lombok/issues/2592 * Report free space * Report free space * Allow heavy tests to be started manually * Allow heavy tests to be started manually * Allow heavy tests to be started manually * Allow heavy tests to be started manually * OBG-959 hbci payment status magic numbers meaning changed * OBG-1177. AIS-embedded postman scripts * # Conflicts: # pom.xml * OBG-1002. Updating sandbox * OBG-1002. Updating sandbox * OBG-1002. Sandbox tests fixes * OBG-1002. Make checkstyle happy * OBG-1002. Make PMD happy * OBG-1002. Fixed random test failure * Bump guava from 28.1-jre to 29.0-jre Bumps [guava](https://github.com/google/guava) from 28.1-jre to 29.0-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) Signed-off-by: dependabot[bot] * OBG-1174. Keep default HBCI urls * Fixed compilation issue * Remove unused imports * Bump guava * Hack for https://github.com/adorsys/open-banking-gateway/issues/1199 * Hack for https://github.com/adorsys/open-banking-gateway/issues/1199 * Hack for https://github.com/adorsys/open-banking-gateway/issues/1199 * Initial modularized analytics * Added result post-processor * Added dummy-mapper * Refactored post-processor application * Fixed POM's * Integrate smart analytics via annotation * Use smartanalytics snapshot * Added dummy mapper * Propagate analytics through API * Propagate analytics through API * Propagate analytics through API * Fixed spring-security leaking from smartanalytics and mapping issues * Propagate analytics result to API * Initial rule resources * Remove remote-resources plugin * Use released smartanalytics * Chain post-processors * Log free space * Bump ubuntu version * Attempt to cleanup overlay2 * Pin postgres version * Added hack for Sandbox message handling * Fix FireFly exporter build * photo otp * Switch to CodeCov action * Fixed tests * Update DEV CodeCov pattern * Fixed test * Initial consent error handling * Initial consent error handling * Improved mapping handling * Implemented consent DELETE Api * Update UI API and add 'Delete in Fintech' button * Make checkstyle happy * OBG-1209 extract sca auth mappings to application properties, test * OBG-1209 fix test * OBG-1209. Small fix * Fixed tests * Add logging to all classes that implement JavaDelegate * fixes in mappers, pmd fixes, deleted unused imports * Bump adapter and cleanup DB blocker * Bump adapter and cleanup DB blocker * Bump adapter and cleanup DB blocker * Added logging * Update adapter bean dependencies * fixes in mappers, xs2a and hbci logresolvers reactoring, added logging of responses and requests * Add custom INTEG deployer * Fixed HBCI logging * Use statuc mapper * Log response for AIS * Allow creating custom consent * Fix mapper and add test * Remove unused imports * Fix UI tests * Fix UI tests * update banks.csv * Fixed missing mapping of grant type in Xs2a adapter * Added Java time module * Use new X-Request-ID * Bugfix/redirect before start (#1233) * Fixed redirects before consent auth start * Set approach from init call * Use special redirection handler * Use special redirection handler * Fixes for payment redirection * Fixed typo * Refactored consent handling * Refactored consent handling * Remove object mapper * Remove object mapper * Remove unnecessary classes * Fixed wrong transaction listing bean * Separate support for skipping start consent authorization * Disable test profiles * add our account iban to hbci transaction mapper (#1230) * Logging in FacadeService and ResultHandler (#1229) * added logging to FacadeService and ResultHandler fixes in XS2A-protocol, HBCI-protocol logging * refactoring, work with comments * refactoring: Constants moved to opba-facade-protocol-api-shared * Fixed FireFly exporter * Resilent smartanalytincs analysis * fixed transaction amount sing for both old and new transactions (#1238) * Bugfix/handle oauth consent (#1256) * Implemented ING quirk * Make checkstyle happy * Revert ConsentOAuth handler * Check bodyLinks for null * Remove unused method * Deepen ING handling * Updated URL mapping * Handle NULL consent id too * Handle NULL consent id too * Handle NULL consent id too * Handle NULL consent id too * Handle consent acquired when consent id is null * Added comment * Fixed mapper * Fixed null consent ID * Simplify dummy consent id handling * [tests] Update wiremock expected URLs to match new pattern * [tests] Fixed another part of issues with redirect code * [tests] Fixed another part of issues with redirect code * [tests] Fixed another part of issues with redirect code * Add possibility to configure consent parameters in ConsentUI (#1236) * Add possibility to configure consent parameters in ConsentUI * fixed date checking in ConsentUI * fixed layout,validation, added form control * fixes according to comments * Normalize paths for windows (#1219) * Normalize paths for windows * Fixes for path normalization on Linux * Fixes for path normalization on Linux * Fixed path mapping * Fixed file separator issue * Feature/simple protocol tester (#1259) * Implemented ING quirk * Make checkstyle happy * Revert ConsentOAuth handler * Check bodyLinks for null * Remove unused method * Deepen ING handling * Updated URL mapping * Handle NULL consent id too * Handle NULL consent id too * Handle NULL consent id too * Handle NULL consent id too * Handle consent acquired when consent id is null * Added comment * Fixed mapper * Fixed null consent ID * Simplify dummy consent id handling * [tests] Update wiremock expected URLs to match new pattern * [tests] Fixed another part of issues with redirect code * [tests] Fixed another part of issues with redirect code * [tests] Fixed another part of issues with redirect code * Added initial layout for testing-helper * Changed package * Renamed helper application * Protocol testing helper application basis implemented * Initialize protocol testing controller * Implemented list accounts handling * Added authorization controller * Renamed authContext getter and fixed search issues * Fixed mappings and added postman collection for Xs2a Sandbox * Make checkstyle happy * Feature/anonymous consent (#1261) * Initial implementation for anonymous consent * Initiated API changes * Fixed tests in FinTech and minor fixes in anonymous consent tests * Fixed configs * Fixed anonymous consent loading * Allow selecting anonymous consent in UI * Unmapped values and checkstyle fixes * Updated API and login controller fixes * Fixed URL path in test * Fixed UI tests * Update PR build script to include FireFly, fix FireFly missing args * Update PR build script to include FireFly, fix FireFly missing args * Pagination for transactions (#1262) * Pagination for transactions * deleted unnecessary code * refactoring * fixes according to comments * Feature/enable extensions (#1264) * Publishing some part of extension binding to OpenSource * Publishing some part of extension binding to OpenSource * More precise protocol loading * Added note about disabling devtools * Added comment * Enabled immediate consent in FinTech * Adding OnLogin action and switching PsuAuth API to async to handle OnLogin action * Adding onLogin call forwarding * Propagate OnLogin action support to upper layers * Finalize OnLogin handler implementation * Added fromAspsp return template * Update mappings for FinAPI * Added comment on config * Renamed pageSize parameter * Extension in separate context * Extension in separate context * Explicit none context * Fixed test * Remove unpublished dependency * Exit on fail * Fixed issue with Fintech server deploy * Postman and other fixes (#1266) * Bugfix/set of minor fixes (#1267) * Postman and other fixes * Increase query range * Feature/delete consent (#1268) * Some initial work for Xs2a * Implemented consent removal for Xs2a * Resolve issues with mappings * Propagate DELETE consent to REST * Added stubs for easier migrations in future * Remove unused import * Fixed mappings, removed stub banks * Added converter for stubs * Remove unused import * Fixed tests * Added postman collections * Rollback stub rows * Fixed mappings and other stuff * Added lost bindings * Fixed mock data handler * Feature/bank search ext (#1269) * Added externalId column * Refactored migrations to use pre-defined contexts * Fixed migrations * Mark sandbox banks explicitly * Added external interfaces declaration * Fixed test * Fixed test * Fixed wrong context * Fixed wrong migration path * Fixed wrong migration path * Fixed tests, use larger column * Added necessary fields * Added missing overrides * Update migration * Update migration * Remove wrong profile * Fixed issues with externalized migrations of liquibase * Fixed issues with externalized migrations of liquibase * Moving smartanalytics to extensions (#1271) * Moving smartanalytics to extensions * Moving smartanalytics to extensions * Change method visibility * Fixed UI test * Add onInit * Fixed UI test * Forcefully set form in test as valid * Disable problematic UI test * Increase webdriver timeout * OBG API clean up (#1270) * OBG API clean up * fixed wiremock tests resources according to cleaned OBG API * returned field "remittanceInformationUnstructured", rephrased endpoint * changed header name (from 'Redirect-Code' to X-XREF-TOKEN), query parameter (from redirectCode to xXrefToken) in ConsentAPI * fixes in IntelliJ Http scripts * fix in Wiremock test * fixed mock date in ConsentUI test * Resolve issue with mocked fixture * Update Postman collections, add smartanalytics enabler * Update Postman collections * Add smartanalytics migration pack * Initialize admin API versioning * Fix for admin tests * Fix issue with JAR based keystore * Сlean up of PIS API (#1279) * clean up of pis api * updated getPaymentStatus test * OBG-1234 Allow fintech pass consent (#1263) * OBG-1234 Allow fintech pass consent * OBG-1234 X-Create-Consent-If-None header as String instead of Object * OBG-1234 add example in api, consent hardcoded in consent-ui * OBG-1234 fixed hardcoded consent object * OBG-1234 fix mapper, add tests * OBG-1234 fix ui-tests * OBG-1234 make consent optional in fintech-ui * OBG-1234 fix consent-ui behavior when fintech doesn't sends consnet header * OBG-1234 add custom consent field in fintech-ui * OBG-1234 add e2e test with consent from fintech * Fixed issue with empty object assertion * Fixed wrong variable mapping * Change defaults to correct values * Feature/date filter on cache (#1281) * Adding date filtering - initial refactor * Generify cache access * Generify cache access * Fix some point locations * Added test for HBCI * Basic Xs2a refactored caching * Share public key too * Added test for authenticated user * Small fixes * Wrong result type fixed * Fixes in test * Fixed checskstyle issues * Updated test fixtures * Fixed anonymous test * Fixed HBCI test and HBCI sandbox booking date * Drop package-lock.json * Add query parameter to delete consent request (#1282) * Fixed smoke test * Feature/bank to bank profile (#1283) * Initialize bank-to-profile mapping * Fixed mappings * Resolve FinTech issues * Updated tests * Updated API definition + migrations * Updated API definition + migrations * Make checkstyle happy * Fixed tests * Fixed update tests * Restructure DB migrations * Restructure DB migrations * Restructure DB migrations * Restructure DB migrations * Further test fixing for restructured DB migrations * Further test fixing for restructured DB migrations * Fixed FinTech tests * Fixed Firefly exporter build * Update FinTech UI * Make checkstyle happy * Update migration and fix tests * Fix UI test * Drop SQL logging * Updated FinTech smoke tests * Fixed smoke tests * Drop unnecessary mapping * Optional profiles * Update Postman tests * Optional profiles * OPEN-3. Import existing user data (#1285) * OPEN-3. Import existing user data * OPEN-3. Added more encryption services * OPEN-3. Added more encryption services * Obg 35: fix DKB Adapter (#1284) * fixed DKB adapter * OBG-35 add Bearer * OBG-35 fix var name * OBG-35 switch to embedded * OBG-35 switch to embedded * OBG-35 switch to embedded * rebase * fix checkstyle errors * OBG-35 set scaAvailableMethods in StartConsentAuthorization * OBG-35 return Embedded * OBG-35 get selected sca type base on user selected sca Id * OBG-35 embedded for dkb adapter liquibase changeset * fixed DKB adapter * OBG-35 add Bearer * OBG-35 fix var name * OBG-35 switch to embedded * OBG-35 switch to embedded * OBG-35 switch to embedded * rebase * fix checkstyle errors * OBG-35 set scaAvailableMethods in StartConsentAuthorization * OBG-35 return Embedded * OBG-35 get selected sca type base on user selected sca Id * OBG-35 embedded for dkb adapter liquibase changeset * cleanup embedded pre step stubs * OBG-35: cleanup and fix reviews comments * OBG-35 handle wrong password for DKB * OBG-35 fix conflicts * OBG-35. Some fixes * OBG-35. Update flows * OBG-35 dkb payment test * OBG-35 dkb payment test * OBG-35 pmd fix * OBG-35 fix changeset context Co-authored-by: Clovis Gakam Co-authored-by: Maxim Grischenko Co-authored-by: Clovis Gakam Co-authored-by: valb3r * Feature/obg 809 decoupled approach updated (#1286) * OBG 809: Table and entity added * OBG 809: Tests extended * OBG 809: Waiting service added * OBG 809: Bpmn diagrams updated with timers * OBG 809: Unnecessary Db and service changes rollback * OBG 809: Bpmn updated, feign client added * OBG 809: Sca Decoupled is added and set in steps * OBG 809: Decoupled switch added * OBG 809: Payment authorization bpmn updated * OBG 809: Payment authorization bpmn spaces refactored * OBG 809: Unused files removed * OBG 809: Decoupled bpmn switch updated * OBG 809: Ais switch added * OBG 809: Ais subproccess added * OBG 809: Ais subproccess updated, paddings added * OBG 809: Ais/pis bpmn refactoring * OBG 809: Ais/pis bpmn refactoring 2 * OBG 809: Ais bpmn refactoring * OBG 809: Ais/Pis bpmn refactoring 3 * OBG 809: Ais/Pis bpmn beans updated * OBG 809: properties added * OBG 809: Github discussions resolved * OBG 809: Checkstyle fixes * OBG 809: typos removed, tests updated * OBG 809: bpmn typos fixed * OBG 809: stages check method type for decoupled SCA * OBG-809. Fixed diagrams * OBG 809: Fix tests * OBG 809: decoupled stages check fixed, property names updated in bpmn files * OBG 809: durations changed to constant values * OBG 809: added extractor for decoupled stages * OBG 809: property configuration added * OBG 809: NPE fixed, app properties fixed for tests * OBG 809: Consent authorization bpmn target ref corrected * OBG 809: Added redirection to a wating page * OBG 809: Redirection payment bpmn change * OBG 809: Waiting mock pages added ais/pis * OBG 809: One Sca flow fixed not to go as zero SCA * OBG 809: BPMN service key fix * OBG 809: Added SCA status setting * OBG 809: unused properties removed * Some merge updates * OBG-809. Resolved build issues * OBG-809. Re-merged diagrams * OBG-809. Updated timer definition and poll interval * OBG-809. Updated decoupled process recognition * OBG-809. Fixed multi SCA case * OBG-809. Updated status retrieval * OBG-809. Adapt flows for API calls * OBG-809. Rollback key pinning * OBG-809. Dropped unused component and update stuff * Fixed redirectCode mappings * Fixed process layouts * OBG-809. Update auth flows * OBG-809. Update controller and mapping * OBG-809. Update UI and mappings * OBG-809. Update mappers * OBG-809. Fixed checkstyle issue * OBG-809. Fixed test issue * OBG-809. Drop UI tests * OBG-809. Added test stubs and fixes * OBG-809. Added test stubs and fixes * OBG-809. Removed Flowable warnings * OBG-809. Fixed concurrency issue with requests from UI * OBG-809. Fixed concurrency issue with requests from UI * OBG-809. Fixed compilation issue * OBG-809. Fixed tests * OBG-809. Fixed PMD * OBG-809. Fixed payment test part * OBG-809. Fixed payment test part * OBG-809. Decoupled payment tests * OBG-809. Support validation rule for decoupled * OBG-809. PMD and checkstyle happy * OBG-809. Support validation rule for decoupled * OBG-809. Restore url Co-authored-by: iiuex * Remove unnecessary logging * Remove unnecessary logging * OBG-43: Fix Postbank adapter integration and add payments and accounts e2e tests (#1289) * fix postbank adapter integration and add payments and accounts e2e tests * cleanup pom * revert fintech ui changes * cleanup and fix reviews conversations * fix formating issues * fix merged conflicts * OBG-46: Add consorsbank e2e tests (#1291) * add consorsbank e2e tests * fix pmd code duplication * fix merged conflicts * fix merged conflicts * Add query parameter to bankSearch endpoints, (#1290) * Add query parameter to bankSearch endpoints, add isActive field to bank, bankProfile, add migration * Included migration to master * fixed column names * fixed test * fixed fireflyExporter, mappers * fixed query to get desired banks directly from DB fixed var names to fix bug with DB data mapping fixes in api deleted deprecated filter from service * fixed query (selected field name) * add isActive to AdminApi etc. * fixed queries in all ymls * Force using wrapper instead of maven * Force using wrapper instead of maven * Feature/open 11 get consent status (#1292) * Remove unnecessary logging * OPEN-11. Auth session status persistence * OPEN-11. Added basic API definition * OPEN-11. Updated Springdoc UI * OPEN-11. Added status bindings * OPEN-11. Get consent status services * OPEN-11. Fix logging issues and handle status correctly * OPEN-11. Add PIS status handling too * OPEN-11. Merged with develop * OPEN-11. Support 'lastRequestId' * OPEN-11. Fixed test * OPEN-11. Added tests * OPEN-11. Rename method * OBG-51: Fix patch admin banks api (#1293) * fix patch admin bank api * fix review conversations * Obg 34 fix sparkasse list transactions (#1288) * OBG-34 add content type JSON as standart xs2a header * OBG-34 getTransactionListAsString as fallback * OBG-34 content type of transactions in bank profile, xml transactions parser * OBG-34 fix checkstyle, improve mapper * OBG-34 add rawTransactions field in Api * OBG-34 sparkasse list transactions wiremock test * OBG-34 fix sparkasse wiremock test * OBG-34 fix test * OBG-34 after rebase fix * Remove unused configuration property (#1297) * OBG-51: Fix admin delete api (#1295) * OBG-51: Fix admin delete api * OBG-51: add e2e tests for delete bank admin api * OPEN-51. Added PIS delete bank test * OPEN-51. Slight reformatting * OPEN-51. Slight reformatting * OPEN-51. Fix db cleanup Co-authored-by: valb3r * OPEN-11. Add protocol status handler for FinAPI (#1298) * OPEN-11. Add protocol status handler for FinAPI * Fixed migrations * OPEN-11. Handle consent details * Feature/open 11 add consent status details (#1299) * OPEN-11. Handle error results from protocol * OPEN-11. Handle consent details * OPEN-11. Handle consent details * OPEN-100. Added note (#1300) * OPEN-98. Support more FinAPI parameters (#1301) * Add paging support for xs2a-protocol (#1296) * add paging support for xs2a-protocol * fix bug with setting null paging values * fixes according to comments added const, added separate test for paging * OBG-42. add Santander e2e tests (#1302) * OBG-42. add Santander bank Account list e2e test * OBG-42. moved Satander stubs into oauths integrated folder * OBG-42. add santander payment e2e test * Feature/open 99 support for external sync (#1303) * OPEN-99. Support force-connection update method * OPEN-98. Make checkstyle happy * OPEN-99. Added extra enum mapping * OPEN-99. Added migration * OBG-95 Document the usage of PSU-IP-Address and Compute-PSU-IP-Address headers (#1305) * Add headers to api. Fix controllers, tests. * Fix header names, e2e tests with computing ip. * Fix firefly services according to updated OBG API * Feature/open 102 bank consent type (#1306) * OPEN-102. Initialize consent type support * OPEN-102. Use multiple types * OPEN-102. Some cleanup, remove custom consent * OPEN-102. Updated UI accordingly * OPEN-102. Updated UI accordingly - use auto-selected * OPEN-102. Fixed migration * OPEN-102. Fixed UI test * OPEN-102. Fixed UI test * Skip start auth consors bank (#1304) * skip start authorisation for consors bank * remove unneeded stubs * OPEN-102. Fixed consent UI branch * Feature/open 30 add external ids (#1309) * OPEN-30. Added external IDs to API * OPEN-30. Added external IDs entities * OBG-41. fix Targo Bank Adapter Integration and Add AIS e2e test. (#1308) * update ci * OBG-41. fix Targo Bank Adapter Integration and Add Ais e2e test. * O1. fix failing tests * OB add targo bank Pis e2e test * OBG-41. cleanup * OBG-41. fix reviews comments * cleanup * Add config values * Add config values * Feature/update documentation (#1310) * Add notice about License change * Add notice about License change * Added links + big picture * OBG-50. add sandbox ais and pis e2e test embedded with one sca (#1312) * OBG-36 OBGBankSearch API returns null (#1311) * Add blank keyword check to bankSearchGET() in TppBankSearchController * Fixes according to comments, fixes in agruments order * Fixed date mapping in transactions from camt (#1313) * OBG-38_volksbank_test (#1307) * OBG-38_volksbank_test * OBG-38 authorization with pin for fiducia * OBG-38 required/new scenario state in stubs * OBG-38 add test getTransactions * OBG-38 migration sequence numbers fix, formatting fix * method name fixes * Bump version to release Co-authored-by: Maxim Grischenko Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: peter Co-authored-by: tsehelnyk <58398447+tsehelnyk@users.noreply.github.com> Co-authored-by: Maxim Grischenko Co-authored-by: Clovis Gakam Co-authored-by: Clovis Gakam Co-authored-by: Clovis Gakam Co-authored-by: iiuex Co-authored-by: Clovis Gakam --- .env | 1 + .github/workflows/daily-heavy-tests.yml | 13 +- .github/workflows/deploy-to-integ.yml | 69 + .github/workflows/develop-push-merge.yml | 37 +- .github/workflows/manual-heavy-tests.yml | 39 + .github/workflows/pull-requests.yml | 15 +- .../workflows/tag-release-candidate-push.yml | 11 +- .github/workflows/tag-release-push.yml | 10 +- .gitignore | 6 + README.md | 122 +- consent-ui/src/app/ais/ais-routing.module.ts | 2 + consent-ui/src/app/ais/ais.module.ts | 6 +- .../account-details.component.html | 1 - .../account-details.component.spec.ts | 25 - .../account-details.component.ts | 15 - consent-ui/src/app/ais/common/consent-util.ts | 14 + .../src/app/ais/common/constant/constant.ts | 3 + consent-ui/src/app/ais/common/date-util.ts | 25 + .../src/app/ais/common/dto/ais-consent.ts | 11 + .../consent-info/consent-info.component.ts | 1 - .../accounts-consent-review.component.html | 53 +- .../accounts-consent-review.component.ts | 53 +- .../entry-page-accounts.component.ts | 10 +- .../dedicated-access.component.spec.ts | 2 +- .../dedicated-access.component.ts | 9 +- ...nt-account-access-selection.component.html | 2 +- ...account-access-selection.component.spec.ts | 3 +- ...sent-account-access-selection.component.ts | 48 +- .../consent-initiate.component.ts | 7 +- .../consent-sharing.component.ts | 21 +- .../entry-page-transactions.component.ts | 12 +- ...transactions-consent-review.component.html | 53 +- ...nsactions-consent-review.component.spec.ts | 2 + .../transactions-consent-review.component.ts | 74 +- .../ais/result-page/result-page.component.ts | 23 +- .../to-aspsp-redirection.component.ts | 26 +- .../wait-for-decoupled.html | 10 + .../wait-for-decoupled.scss} | 0 .../wait-for-decoupled/wait-for-decoupled.ts | 64 + .../app/api-auth/.openapi-generator/VERSION | 2 +- consent-ui/src/app/api-auth/README.md | 25 + consent-ui/src/app/api-auth/api.module.ts | 6 +- .../api-auth/api/psuAuthentication.service.ts | 123 +- ...uthenticationAndConsentApproval.service.ts | 196 +- consent-ui/src/app/api/api.headers.ts | 1 + .../authStateConsentAuthorization.service.ts | 16 +- .../api/api/consentAuthorization.service.ts | 400 - .../fromASPSPConsentAuthorization.service.ts | 35 +- .../api/updateConsentAuthorization.service.ts | 134 +- .../src/app/api/model/authorizeRequest.ts | 21 - consent-ui/src/app/api/model/consentAuth.ts | 8 + .../app/api/model/consentAuthRequiredField.ts | 35 - .../app/api/model/consentAuthRequirement.ts | 21 - .../src/app/api/model/denyRedirectRequest.ts | 33 - consent-ui/src/app/api/model/denyRequest.ts | 33 - .../src/app/api/model/inlineResponse200.ts | 18 - consent-ui/src/app/api/model/models.ts | 4 - consent-ui/src/app/api/model/scaUserData.ts | 1 + .../anonymous/anonymous.component.spec.ts | 2 +- .../app/auth/anonymous/anonymous.component.ts | 2 +- .../app/auth/login/login.component.spec.ts | 8 +- .../src/app/auth/login/login.component.ts | 2 +- consent-ui/src/app/common/auth.service.ts | 25 +- .../common/enter-pin/enter-pin.component.ts | 3 +- .../enter-tan/enter-tan.component.spec.ts | 4 +- .../common/enter-tan/enter-tan.component.ts | 11 +- .../common/select-sca/select-sca.component.ts | 7 +- consent-ui/src/app/common/session.service.ts | 14 + consent-ui/src/app/common/utils/stub-util.ts | 1 - ...sent-payment-access-selection.component.ts | 24 +- .../initiation/payment-initiate.component.ts | 2 +- .../payments-consent-review.component.ts | 3 +- consent-ui/src/app/pis/pis-routing.module.ts | 2 + consent-ui/src/app/pis/pis.module.ts | 4 +- .../pis/result-page/result-page.component.ts | 5 +- .../to-aspsp-page/to-aspsp-page.component.ts | 26 +- .../wait-for-decoupled.html | 10 + .../wait-for-decoupled.scss | 0 .../wait-for-decoupled/wait-for-decoupled.ts | 64 + consent-ui/src/styles.scss | 24 + docker-compose.yml | 10 +- docs/img/big-picture.png | Bin 0 -> 147788 bytes fintech-examples/fintech-api/pom.xml | 2 +- .../src/main/resources/static/fintech_api.yml | 162 +- fintech-examples/fintech-db-schema/pom.xml | 2 +- .../fintech-example-tests/pom.xml | 2 +- .../e2e/FintechConsentUiSmokeE2ETest.java | 14 +- .../tests/e2e/FintechPaymentSmokeE2ETest.java | 6 +- .../tests/e2e/steps/FintechServer.java | 4 +- .../tests/e2e/steps/FintechStagesUtils.java | 5 +- .../steps/WebDriverBasedUserInfoFintech.java | 25 +- .../application-test-smoke-fintech.yml | 2 +- fintech-examples/fintech-impl/pom.xml | 2 +- .../FinTechAccountInformationImpl.java | 30 +- .../controller/FinTechBankSearchImpl.java | 4 +- .../repositories/ConsentRepository.java | 4 + .../fintech/impl/service/AccountService.java | 78 +- .../impl/service/BankSearchService.java | 12 +- .../fintech/impl/service/ConsentService.java | 7 + .../impl/service/HandleAcceptedService.java | 2 +- .../fintech/impl/service/PaymentService.java | 18 +- .../impl/service/TransactionService.java | 38 +- .../opba/fintech/impl/tppclients/Consts.java | 1 + .../impl/service/BankSearchServiceTest.java | 5 +- .../fintech-last-module-codecoverage/pom.xml | 2 +- fintech-examples/fintech-server/Dockerfile | 2 +- fintech-examples/fintech-server/pom.xml | 4 +- .../fintech/server/FinTechApiBaseTest.java | 2 +- .../server/FinTechBankSearchApiTest.java | 18 +- .../server/FinTechListAccountsTest.java | 32 +- .../server/FinTechListTransactionsTest.java | 21 +- .../feignmocks/TppAisClientFeignMock.java | 53 +- .../TppBankSearchClientFeignMock.java | 8 +- ...356938ab-9561-408f-ac7a-a9089c1623b7.json} | 0 .../test/resources/TPP_LIST_TRANSACTIONS.json | 6 + fintech-examples/fintech-ui/angular.json | 25 +- fintech-examples/fintech-ui/package.json | 2 +- .../src/app/api/.openapi-generator/VERSION | 2 +- .../fintech-ui/src/app/api/README.md | 25 + .../fintech-ui/src/app/api/api.module.ts | 11 +- .../fintech-ui/src/app/api/api/api.ts | 4 +- .../api/finTechAccountInformation.service.ts | 279 +- .../api/api/finTechAuthorization.service.ts | 233 +- .../app/api/api/finTechBankSearch.service.ts | 154 +- .../finTechOauth2Authentication.service.ts | 61 +- ...intechRetrieveAllSinglePayments.service.ts | 77 +- .../api/api/fintechRetrieveConsent.service.ts | 137 + .../fintechSinglePaymentInitiation.service.ts | 97 +- .../src/app/api/model/accountBalance.ts | 2 +- .../src/app/api/model/accountDetails.ts | 2 +- .../src/app/api/model/accountReport.ts | 12 +- .../src/app/api/model/aisAccountAccessInfo.ts | 53 + .../src/app/api/model/aisConsentRequest.ts | 37 + .../app/api/model/analyticsReportDetails.ts | 71 + .../src/app/api/model/bankDescriptor.ts | 2 + .../src/app/api/model/bankProfile.ts | 6 + .../fintech-ui/src/app/api/model/models.ts | 1 + .../src/app/api/model/transactionsResponse.ts | 5 + .../src/app/api/model/userProfile.ts | 2 +- .../bank-search/bank-search.component.html | 2 +- .../bank-search/bank-search.component.spec.ts | 12 +- .../app/bank-search/bank-search.component.ts | 46 +- .../list-accounts/list-accounts.component.ts | 70 +- .../list.accounts.component.spec.ts | 4 +- .../list-transactions.component.html | 2 + .../list-transactions.component.ts | 10 +- .../list.transactions.component.spec.ts | 4 +- ...payment-account-payments.component.spec.ts | 2 +- .../payment-accounts.component.ts | 16 +- .../payment-initiate/initiate.component.ts | 39 +- .../src/app/bank/services/ais.service.spec.ts | 11 +- .../src/app/bank/services/ais.service.ts | 17 +- .../app/bank/settings/settings.component.html | 95 +- .../app/bank/settings/settings.component.ts | 52 +- .../fintech-ui/src/app/models/consts.ts | 6 + .../src/app/services/storage.service.ts | 18 +- fintech-examples/pom.xml | 2 +- firefly-exporter/pom.xml | 4 +- .../controller/mvc/ViewController.java | 6 +- .../rest/BankConsentController.java | 8 +- .../controller/rest/ExportController.java | 13 +- .../rest/ExportableAccountsController.java | 7 +- .../controller/rest/SearchController.java | 2 +- .../fireflyexporter/entity/BankConsent.java | 2 +- .../fireflyexporter/entity/RedirectState.java | 2 +- .../repository/BankConsentRepository.java | 5 +- .../service/AccountExportService.java | 13 +- .../service/ConsentService.java | 21 +- .../service/ExportableAccountService.java | 13 +- .../service/FireFlyTransactionExporter.java | 25 +- .../service/TransactionExportService.java | 5 +- .../src/main/resources/application.yml | 1 + ...000-create-firefly-exporter-iii-tables.xml | 4 +- helpers/docker-wiremock.yml | 9 + .../xs2a-sandbox-only/.env | 2 +- .../xs2a-sandbox-only/docker-compose.yml | 84 +- last-module-codecoverage/pom.xml | 2 +- opba-admin-rest-api/pom.xml | 2 +- .../main/resources/static/tpp_admin_api.yml | 53 +- opba-admin-rest-impl/pom.xml | 2 +- .../adminapi/service/AdminApiService.java | 116 +- opba-analytics/opba-analytics-api/pom.xml | 27 + .../adorsys/opba/analytics/GlobalConst.java | 16 + .../opba/analytics/TransactionAnalyzer.java | 33 + .../opba/analytics/dto/AnalyticsRequest.java | 18 + opba-analytics/pom.xml | 26 + .../pom.xml | 2 +- .../pom.xml | 2 +- .../resources/api/tpp_banking_api_ais.yml | 59 + opba-api-security-signer-generator/pom.xml | 2 +- opba-api-security/pom.xml | 2 +- .../internal/filter/RequestCookieFilter.java | 3 +- opba-auth-rest-api/pom.xml | 3 +- .../main/resources/static/tpp_auth_api.yml | 33 +- opba-auth-rest-impl/pom.xml | 2 +- .../controller/PsuAuthController.java | 60 +- .../controller/PsuAuthControllerTest.java | 18 +- opba-banking-protocol-facade/pom.xml | 7 +- .../facade/config/auth/FacadeAuthConfig.java | 3 + .../config/encryption/CmsEncryptionOper.java | 17 + .../PsuEncryptionServiceProvider.java | 5 + .../impl/fintech/FintechSecureStorage.java | 26 +- .../encryption/impl/psu/PsuSecureStorage.java | 17 +- .../protocol/facade/dto/PubAndPrivKey.java | 13 + ...RuntimeErrorResultWithOwnResponseCode.java | 15 +- .../facade/services/AuthSessionHandler.java | 21 +- .../facade/services/EncryptionKeySerde.java | 43 +- .../services/FacadeOptionalService.java | 54 + .../facade/services/FacadeService.java | 60 +- .../GetAuthorizationStatusService.java | 70 + .../services/ProtocolResultHandler.java | 121 +- .../facade/services/ProtocolSelector.java | 20 +- .../services/ais/DeleteConsentService.java | 30 + .../ais/GetAisAuthorizationStatusService.java | 70 + .../ais/UpdateExternalAisSessionService.java | 31 + .../authorization/OnLoginService.java | 49 + .../authorization/PsuLoginForAisService.java | 77 - .../authorization/PsuLoginService.java | 108 + .../psuauth/PsuFintechAssociationService.java | 5 +- .../ServiceContextProviderForFintech.java | 20 +- .../fintech/FintechAuthenticator.java | 10 +- .../fintech/registrar/FintechRegistrar.java | 3 +- .../pis/GetPisAuthorizationStatusService.java | 64 + .../scoped/IgnoreFieldsLoaderFactory.java | 17 + .../scoped/IgnoreValidationRuleImpl.java | 2 + .../scoped/RequestScopedProvider.java | 20 +- .../AnonymousPsuConsentAccess.java | 92 + .../consentaccess/ConsentAccessFactory.java | 13 +- ...ess.java => FintechConsentAccessImpl.java} | 51 +- .../ProtocolFacingConsentImpl.java | 10 + .../paymentaccess/FintechPaymentAccess.java | 9 +- .../util/logresolver/FacadeLogResolver.java | 82 + .../util/logresolver/domain/ActionLog.java | 28 + .../domain/ProtocolWithCtxLog.java | 38 + .../domain/context/ServiceContextLog.java | 57 + .../request/FacadeServiceableRequestLog.java | 45 + .../domain/request/RequestLog.java | 49 + .../domain/response/ResultLog.java | 47 + .../services/ais/ListAccountsServiceTest.java | 2 +- .../ais/ListTransactionsServiceTest.java | 2 +- .../ais/ServiceContextProviderTest.java | 8 +- .../AbstractServiceSessionTest.java | 6 +- .../src/test/resources/application-test.yml | 3 +- opba-banking-rest-api-ymls/pom.xml | 2 +- .../resources/static/tpp_banking_api_ais.yml | 296 +- .../static/tpp_banking_api_bank_search.yml | 60 +- .../static/tpp_banking_api_commons.yml | 239 +- .../resources/static/tpp_banking_api_pis.yml | 75 +- .../static/tpp_banking_api_token.yml | 16 - opba-banking-rest-api/pom.xml | 2 +- opba-banking-rest-impl/pom.xml | 2 +- .../controller/TppBankSearchController.java | 32 +- .../TppBankingApiAisController.java | 156 +- .../TppBankingApiPisController.java | 10 +- .../TppBankingApiPisInfoController.java | 46 +- .../tppbankingapi/controller/UuidMapper.java | 19 + .../tppbankingapi/service/BankService.java | 10 +- .../service/ConsentConfirmationService.java | 12 +- .../service/PaymentConfirmationService.java | 6 +- .../adorsys/opba/db/domain/MappersTest.java | 7 +- .../TestTppBankSearchController.java | 40 +- .../service/BankServiceTest.java | 2 +- .../resources/application-test-search.yml | 3 + .../src/test/resources/application.yml | 16 +- ...o_rest_response_mapper_accounts_input.json | 2 - opba-consent-rest-api/pom.xml | 2 +- .../main/resources/static/tpp_consent_api.yml | 181 +- opba-consent-rest-impl/pom.xml | 2 +- .../FromAspspConsentServiceController.java | 10 +- .../UpdateAuthConsentServiceController.java | 26 +- .../consentapi/service/FromAspspMapper.java | 4 +- .../service/mapper/AisConsentMapper.java | 1 + .../service/mapper/AisConsentMapperTest.java | 51 + opba-db/README.md | 10 +- opba-db/mock-data-generate.groovy | 114 + opba-db/pom.xml | 11 +- .../converter/ResultContentTypeConverter.java | 29 + .../converter/ScaApproachConverter.java | 16 +- .../SupportedConsentTypeConverter.java | 36 + .../adorsys/opba/db/domain/entity/Bank.java | 16 +- .../opba/db/domain/entity/BankAction.java | 2 +- .../opba/db/domain/entity/BankProfile.java | 43 +- .../opba/db/domain/entity/Consent.java | 37 +- .../opba/db/domain/entity/Payment.java | 2 +- .../opba/db/domain/entity/ValidationRule.java | 2 + .../db/domain/entity/helpers/UuidMapper.java | 20 + .../domain/entity/sessions/AuthSession.java | 41 + .../entity/sessions/ServiceSession.java | 4 + .../repository/BankProfileRepositoryImpl.java | 21 +- .../repository/BankSearchRepositoryImpl.java | 30 +- .../repository/jpa/BankActionRepository.java | 5 +- .../jpa/BankProfileJpaRepository.java | 17 +- .../db/repository/jpa/BankRepository.java | 3 +- .../db/repository/jpa/ConsentRepository.java | 2 + .../db/repository/jpa/PaymentRepository.java | 2 + .../FintechPsuAspspPrvKeyInboxRepository.java | 2 + .../FintechPsuAspspPrvKeyRepository.java | 2 + .../jpa/psu/PsuAspspPrvKeyRepository.java | 2 + .../src/main/resources/migration/master.xml | 18 +- .../migrations/0000-create-table-banks.xml | 61 +- .../migrations/0001-init-protocol-facade.xml | 25 +- .../migration/migrations/0002-mock-data.xml | 302 +- .../0003-add-staging-bank-configuration.xml | 74 +- .../0004-add-skip-auth-for-deutsche-bank.xml | 15 + .../0005-enable-finapi-test-bank.xml | 29 + .../migrations/0009-add-consent-cache.xml | 12 + .../migrations/0010-update-dkb-adapter.xml | 11 + .../0011-add-validation-ignore-decoupled.xml | 13 + .../0012-add-is-active-field-to-bank.xml | 23 + .../0013-add-auth-session-status.xml | 13 + .../0014-update-sparkasse-adapter.xml | 11 + .../0017-add-consent-type-support.xml | 18 + ...0018-add-skip-psu-auth-for-verlag-bank.xml | 16 + ...et-start-consent-auth-with-pin-fiducia.xml | 15 + .../migration/migrations/bank_action_data.csv | 43345 ---------------- .../migrations/bank_profile_data.csv | 3613 -- .../migrations/bank_sub_action_data.csv | 28897 ----------- .../migration/migrations/csv/v0/0-banks.csv | 3546 ++ .../migrations/csv/v0/0-hbci_bank-action.csv | 28361 ++++++++++ .../csv/v0/0-hbci_bank-sub-action.csv | 14181 +++++ .../csv/v0/0-hbci_bank_bank-profile.csv | 3546 ++ .../migrations/csv/v0/0-mock-banks.csv | 5 + .../migrations/csv/v0/0-mock_bank_profile.csv | 9 + .../migrations/csv/v0/0-xs2a_bank-action.csv | 28361 ++++++++++ .../migrations/csv/v0/0-xs2a_bank-profile.csv | 3546 ++ .../csv/v0/0-xs2a_bank-sub-action.csv | 14181 +++++ .../{ => csv/v0}/banks_random_data.csv | 0 .../v0/xs2a-adapter-banks.csv} | 4715 +- .../BankProtocolActionsSqlGeneratorTest.java | 169 +- opba-embedded-starter-tests/pom.xml | 2 +- .../opba/starter/AdminApiModyfingTest.java | 68 +- .../opba/starter/AdminApiSecurityTest.java | 2 +- .../de/adorsys/opba/starter/AdminApiTest.java | 8 +- .../starter/BasicOpenBankingStartupTest.java | 26 +- .../test/resources/adapter.config.properties | 3 - ...55c80-fd8c-42f0-b782-8188662aa89f-new.json | 7 +- ...a-f7fd-4c9f-a6bd-7a9e12aeee76-updated.json | 43 - ...00-0000-b0b0b0b0b0b0-profiles-removed.json | 9 + ...0-0000-b0b0b0b0b0b0-profiles-replaced.json | 46 + ...d-1000-0000-0000-b0b0b0b0b0b0-updated.json | 82 + ...adadadad-1000-0000-0000-b0b0b0b0b0b0.json} | 50 +- ...0-0000-b0b0b0b0b0b0-profiles-replaced.json | 368 + .../application-test-separate-db.yml | 3 + .../src/test/resources/application-test.yml | 2 + opba-embedded-starter/pom.xml | 2 +- .../opba/starter/config/WebConfig.java | 2 + .../extensions/OpbaFinapiExtensionConfig.java | 12 + .../OpbaSmartAnalyticsExtensionConfig.java | 12 + .../main/resources/adapter.config.properties | 5 +- .../src/main/resources/application.yml | 63 +- opba-facade-protocol-api-shared/pom.xml | 2 +- .../adorsys/opba/protocol/api/Constants.java | 19 + .../opba/protocol/api/common/Approach.java | 3 +- .../api/common/CurrentBankProfile.java | 32 +- .../protocol/api/common/ProtocolAction.java | 29 +- .../api/common/ResultContentType.java | 21 + .../protocol/api/common/SessionStatus.java | 37 + .../api/common/SupportedConsentType.java | 20 + .../protocol/api/dto/NotSensitiveData.java | 7 + .../protocol/api/dto/context/Context.java | 15 +- .../api/dto/parameters/ExtraRequestParam.java | 2 + .../dto/request/FacadeServiceableRequest.java | 13 +- .../AisAuthorizationStatusRequest.java | 25 + .../UpdateExternalAisSessionRequest.java | 25 + .../authorization/DeleteConsentRequest.java | 30 + .../request/authorization/OnLoginRequest.java | 25 + .../dto/request/payments/PaymentInfoBody.java | 1 + .../request/payments/PaymentStatusBody.java | 1 + .../PisAuthorizationStatusRequest.java | 25 + .../transactions/ListTransactionsRequest.java | 10 + .../result/body/AccountListDetailBody.java | 29 +- .../api/dto/result/body/AccountReference.java | 13 +- .../api/dto/result/body/AccountReport.java | 9 +- .../body/AisAuthorizationStatusBody.java | 21 + .../api/dto/result/body/AnalyticsResult.java | 37 + .../api/dto/result/body/AuthStateBody.java | 8 + .../result/body/AuthorizationStatusBody.java | 20 + .../dto/result/body/DeleteConsentBody.java | 11 + .../result/body/DetailedSessionStatus.java | 17 + .../protocol/api/dto/result/body/Paging.java | 33 + .../body/PisAuthorizationStatusBody.java | 21 + .../api/dto/result/body/ResultBody.java | 3 + .../result/body/TransactionDetailsBody.java | 5 +- .../result/body/TransactionsResponseBody.java | 16 +- .../api/dto/result/body/UpdateAuthBody.java | 24 + .../body/UpdateExternalAisSessionBody.java | 13 + .../api/dto/result/fromprotocol/Result.java | 14 +- .../dialog/RedirectToAspspResult.java | 2 +- .../fromprotocol/error/ErrorResult.java | 6 + .../api/services/ResultBodyPostProcessor.java | 9 + .../scoped/consent/FintechConsentAccess.java | 19 + .../scoped/consent/ProtocolFacingConsent.java | 10 + .../hbci-bdd-sandbox/pom.xml | 2 +- ...HbciSandboxConsentE2EHbciProtocolTest.java | 31 +- ...dTransactionsAfterE2EHbciProtocolTest.java | 19 +- ...HbciSandboxPaymentE2EHbciProtocolTest.java | 6 +- .../HbciPaymentInitiationRequest.java | 4 +- opba-protocols/hbci-protocol-tests/pom.xml | 2 +- opba-protocols/hbci-protocol/pom.xml | 2 +- .../hbci/config/HbciScaConfiguration.java | 21 + .../protocol/hbci/context/HbciContext.java | 8 +- .../context/TransactionListHbciContext.java | 12 + .../HbciExtendWithServiceContext.java | 15 +- .../hbci/entrypoint/HbciOutcomeMapper.java | 6 +- .../entrypoint/HbciResultBodyExtractor.java | 6 +- .../HbciTransactionsToFacadeMapper.java | 44 +- .../entrypoint/ais/HbciDeleteConsent.java | 41 + .../ais/HbciListAccountsEntrypoint.java | 2 +- .../ais/HbciListTransactionsEntrypoint.java | 6 +- .../HbciGetAuthorizationState.java | 26 +- .../HbciUpdateAuthorization.java | 15 +- .../HbciInitiateSinglePaymentEntrypoint.java | 2 +- .../pis/HbciPreparePaymentContext.java | 4 +- .../consent/HbciAisConsentService.java | 7 + .../consent/HbciCachedResultAccessor.java | 78 +- .../hbci/service/consent/HbciConsentInfo.java | 4 +- .../HbciLoadConsentUnderFinTechKey.java | 15 +- .../consent/HbciReadAccountListFromCache.java | 7 + .../HbciReadTransactionListFromCache.java | 11 +- .../consent/HbciStoreAccountListToCache.java | 11 +- .../HbciStoreTransactionListToCache.java | 16 +- .../ReportConsentAuthorizationFinished.java | 4 + .../consent/authentication/HbciAskForPin.java | 6 + .../consent/authentication/HbciAskForTan.java | 6 + .../HbciAskToSelectTanChallenge.java | 7 + .../HbciInitiateSendPinAndPsuId.java | 8 +- .../HbciReportConsentIncompatible.java | 6 + .../authentication/HbciSendTanChallenge.java | 8 + ...itSelectedMethodAndAskForTanChallenge.java | 11 + .../service/protocol/HbciFillBpmnContext.java | 6 + .../HbciUpdateToRealModeBpmnContext.java | 7 + .../protocol/ais/HbciAccountListing.java | 11 +- .../protocol/ais/HbciTransactionListing.java | 10 +- .../protocol/ais/dto/HbciResultCache.java | 21 +- .../publish/HbciPublishAccountListResult.java | 4 + .../HbciPublishTransactionListResult.java | 16 + .../service/protocol/pis/HbciPayment.java | 10 + .../protocol/pis/HbciPaymentStatus.java | 9 +- .../protocol/pis/HbciPersistPaymentToDb.java | 6 +- .../HbciPublishPaymentStatusResult.java | 4 + .../validation/HbciReportValidationError.java | 7 + .../HbciRestorePreValidationContext.java | 8 + .../HbciStorePreValidationContext.java | 7 + .../util/logresolver/HbciLogResolver.java | 149 + .../domain/AccountListHbciContextLog.java | 22 + .../logresolver/domain/BaseContextLog.java | 23 + .../logresolver/domain/HbciContextLog.java | 45 + .../logresolver/domain/HbciExecutionLog.java | 14 + .../domain/PaymentHbciContextLog.java | 27 + .../domain/TransactionListHbciContextLog.java | 25 + .../domain/request/RequestLog.java | 43 + .../domain/request/TransactionRequestLog.java | 42 + .../domain/response/ResponseLog.java | 38 + .../mapper/HbciDtoToLogObjectsMapper.java | 32 + .../hbci-list-transactions.bpmn20.xml | 4 +- .../HbciUpdateAuthorizationTest.java | 75 + .../src/test/resources/application-test.yml | 10 + opba-protocols/opba-protocol-api/pom.xml | 2 +- .../opba/protocol/api/ais/DeleteConsent.java | 20 + .../api/ais/GetAisAuthorizationStatus.java | 11 + .../api/ais/UpdateExternalAisSession.java | 11 + .../protocol/api/authorization/OnLogin.java | 20 + .../api/errors/ProcessErrorConsentGone.java | 9 + .../api/errors/ProcessErrorStrings.java | 11 - .../api/errors/ReturnableException.java | 8 + .../api/pis/GetPisAuthorizationStatus.java | 11 + .../opba-protocol-testing-helper/Dockerfile | 10 + .../opba-protocol-testing-helper/README.md | 22 + .../opba-protocol-testing-helper/pom.xml | 127 + ...LPER-XS2A-EMBEDDED.postman_collection.json | 355 + .../OpenBankingTestingHelperApplication.java | 21 + .../protocol/testing}/config/DevProfile.java | 2 +- .../protocol/testing/config/WebConfig.java | 18 + .../controller/ProtocolTestingController.java | 119 + .../service/MapBasedAspspRepository.java | 56 + ...MapBasedRequestScopedServicesProvider.java | 198 + .../main/resources/adapter.config.properties | 12 + .../src/main/resources/application.yml | 144 + .../src/main/resources/logback.xml | 17 + .../src/main/resources/sample-qwac.keystore | Bin 0 -> 5867 bytes .../suppressions.xml | 9 + opba-protocols/pom.xml | 4 +- opba-protocols/protocol-bpmn-shared/pom.xml | 2 +- .../flowable/FlowableJobEventListener.java | 18 +- .../config/flowable/SerializerUtil.java | 3 +- ...IncompatibleWithValidationErrorResult.java | 2 +- .../ContextBasedValidationErrorResult.java | 2 +- .../bpmnshared/dto/context/BaseContext.java | 7 +- .../dto/context/ProtocolResultCache.java | 22 + ...rnalReturnableConsentGoneProcessError.java | 18 + .../InternalReturnableProcessError.java | 15 - .../bpmnshared/outcome/OutcomeMapper.java | 4 +- .../service/SafeCacheSerDeUtil.java | 6 +- .../bpmnshared/service/TransactionUtil.java | 20 + .../service/cache/CachedResultAccessor.java | 114 + .../ProcessEventHandlerRegistrar.java | 6 +- .../eventbus/ProcessResultEventHandler.java | 9 +- .../service/exec/ValidatedExecution.java | 19 + .../util/logResolver/LogResolver.java | 40 + .../util/logResolver/domain/ContextLog.java | 36 + .../util/logResolver/domain/ExecutionLog.java | 14 + .../mapper/DtoToLogObjectsMapper.java | 19 + opba-protocols/sandboxes/hbci-sandbox/pom.xml | 2 +- .../sandbox/hbci/config/dto/Transaction.java | 2 +- .../hbci/domain/HbciSandboxPayment.java | 8 +- .../hbci/protocol/OperationHandler.java | 6 + .../common/ExtractBankAndUserIfAvailable.java | 6 + .../hbci/protocol/common/SetDialogId.java | 8 + .../service/HbciSandboxPaymentService.java | 4 +- .../logresolver/HbciSandboxLogResolver.java | 41 + .../domain/HbciSandboxContextLog.java | 18 + .../domain/HbciSandboxExecutionLog.java | 14 + .../HbciSandboxDtoToLogObjectsMapper.java | 16 + .../src/main/resources/application.yml | 2 +- opba-protocols/sandboxes/pom.xml | 2 +- opba-protocols/sandboxes/xs2a-sandbox/pom.xml | 2 +- ...ation-test-db-test-containers-postgres.yml | 3 +- .../application-test-ledgers-gateway.yml | 2 +- .../application-test-aspsp-profile.yml | 1 - .../application-test-db-local-postgres.yml | 3 +- ...ation-test-db-test-containers-postgres.yml | 3 +- .../application-test-ledgers-gateway.yml | 2 +- .../application-test-online-banking.yml | 8 +- .../sandbox/application-test-tpp-rest.yml | 20 +- .../resources/sandbox/prepare-postgres.sql | 2 + opba-protocols/xs2a-protocol-tests/pom.xml | 2 +- .../xs2a-bdd-sandbox/pom.xml | 2 +- .../e2e/sandbox/servers/SandboxServers.java | 2 +- .../tests/e2e/sandbox/SandboxCommonTest.java | 2 +- .../SandboxE2EProtocolAisOauth2Test.java | 94 + .../sandbox/SandboxE2EProtocolAisTest.java | 93 +- .../sandbox/SandboxE2EProtocolPisTest.java | 6 +- .../application-test-mocked-sandbox.yml | 10 +- .../xs2a-bdd-tests-common/pom.xml | 8 +- .../opba/protocol/xs2a/tests/HeaderNames.java | 4 +- .../AccountInformationRequestCommon.java | 210 +- .../e2e/stages/AccountInformationResult.java | 456 +- .../xs2a/tests/e2e/stages/AdminUtil.java | 28 + .../tests/e2e/stages/CommonGivenStages.java | 16 +- .../e2e/stages/NonHappyPaymentResult.java | 8 +- .../e2e/stages/PaymentRequestCommon.java | 81 +- .../xs2a/tests/e2e/stages/PaymentResult.java | 51 +- .../e2e/stages/PaymentStagesCommonUtil.java | 31 +- .../xs2a/tests/e2e/stages/RequestCommon.java | 78 +- .../tests/e2e/stages/RequestStatusUtil.java | 63 + .../tests/e2e/stages/StagesCommonUtil.java | 64 +- .../main/resources/adapter.config.properties | 8 +- ...plication-test-one-time-postgres-ramfs.yml | 46 +- ...ton-brueckner-transactions-no-consent.json | 5 + ...x-musterman-dedicated-account-consent.json | 33 + .../xs2a-bdd-wiremock/pom.xml | 8 +- .../tests/e2e/wiremock/mocks/MockServers.java | 267 +- .../WiremockAccountInformationRequest.java | 17 +- .../mocks/WiremockPaymentRequest.java | 15 +- .../mocks/Xs2aProtocolApplication.java | 3 +- .../application-test-mocked-sandbox.yml | 8 +- .../mappings/mapping-v1-consents-24281.json | 4 +- .../mappings/mapping-v1-consents-24283.json | 4 +- .../mappings/mapping-v1-consents-3.json | 4 +- .../mappings/mapping-v1-consents-4.json | 4 +- .../__files/body-v1-accounts-55313.json | 1 + .../__files/body-v1-consents-2059236999.json | 1 + .../__files/body-v1-consents-3192330705.json | 1 + .../__files/body-v1-consents-3258146603.json | 1 + .../__files/body-v1-consents-3496770420.json | 1 + .../__files/body-v1-consents-370291184.json | 1 + .../mappings/mapping-v1-accounts-55313.json | 30 + .../mapping-v1-consents-2059235614.json | 30 + .../mapping-v1-consents-2059235615.json | 29 + .../mapping-v1-consents-2059236656.json | 30 + .../mapping-v1-consents-3192330705.json | 39 + .../mapping-v1-consents-3258146603.json | 36 + .../mapping-v1-consents-3496770420.json | 30 + .../mapping-v1-consents-370291184.json | 46 + .../targobank/mappings/create-consent.json | 51 + .../targobank/mappings/get-accounts.json | 34 + .../mappings/get-consent-status.json | 30 + .../mappings/get-sca-finalized-status.json | 34 + .../get-sca-scaMethodSelected-status.json | 34 + .../mappings/update-psu-authentication.json | 40 + .../payments/__files/body-v1-1063823569.json | 1 + .../payments/__files/body-v1-1063823571.json | 1 + .../payments/__files/body-v1-3832098252.json | 1 + .../payments/__files/body-v1-492790064.json | 1 + .../payments/__files/body-v1-916399554.json | 1 + ...-payments-sepa-credit-transfers-12221.json | 1 + .../mappings/mapping-v1-1063823569.json | 30 + .../mappings/mapping-v1-1063823570.json | 30 + .../mappings/mapping-v1-1063823571.json | 30 + .../mappings/mapping-v1-3832098252.json | 36 + .../mappings/mapping-v1-492790064.json | 46 + .../mappings/mapping-v1-916399554.json | 36 + ...1-payments-sepa-credit-transfers-4556.json | 29 + .../payments-sct-get-payment-status.json | 30 + ...payments-sct-get-sca-finalized-status.json | 34 + .../mappings/payments-sct-get-sca-status.json | 34 + .../payments-sct-initiate-payment.json | 51 + ...ayments-sct-update-psu-authentication.json | 40 + .../__files/body-v1-accounts-55313.json | 1 + .../__files/body-v1-consents-2059236824.json | 1 + .../__files/body-v1-consents-2059236999.json | 1 + .../__files/body-v1-consents-2145005849.json | 1 + .../__files/body-v1-consents-328937491.json | 1 + .../__files/body-v1-consents-3657373201.json | 1 + .../mappings/mapping-v1-accounts-55313.json | 30 + .../mapping-v1-consents-2059235614.json | 30 + .../mapping-v1-consents-2059235615.json | 29 + .../mapping-v1-consents-2059236656.json | 30 + .../mapping-v1-consents-2059236824.json | 30 + .../mapping-v1-consents-2145005849.json | 36 + .../mapping-v1-consents-328937491.json | 39 + .../mapping-v1-consents-3657373201.json | 46 + .../payments/__files/body-v1-1063823569.json | 1 + .../payments/__files/body-v1-1063823571.json | 1 + .../payments/__files/body-v1-3832098252.json | 1 + .../payments/__files/body-v1-492790064.json | 1 + .../payments/__files/body-v1-916399554.json | 1 + ...-payments-sepa-credit-transfers-12221.json | 1 + .../mappings/mapping-v1-1063823569.json | 30 + .../mappings/mapping-v1-1063823570.json | 30 + .../mappings/mapping-v1-1063823571.json | 30 + .../mappings/mapping-v1-3832098252.json | 36 + .../mappings/mapping-v1-492790064.json | 46 + .../mappings/mapping-v1-916399554.json | 36 + ...1-payments-sepa-credit-transfers-4556.json | 29 + .../postbank/mappings/create-consent.json | 59 + .../mappings/get-accounts-with-balances.json | 37 + .../postbank/mappings/get-accounts.json | 37 + .../postbank/mappings/get-balances.json | 37 + .../postbank/mappings/get-consent-status.json | 34 + .../postbank/mappings/get-transactions.json | 37 + .../postbank/mappings/select-sca-method.json | 46 + .../accounts/postbank/mappings/send-otp.json | 46 + .../mappings/update-psu-authentication.json | 46 + .../__files/response-body-get-accounts.json | 80 + .../response-body-get-transactions.json | 53 + ...esponse-body-start-psu-authentication.json | 39 + .../volksbank/mappings/create-consent.json | 52 + .../volksbank/mappings/get-accounts.json | 42 + .../volksbank/mappings/get-transactions.json | 42 + .../volksbank/mappings/select-sca-method.json | 51 + .../accounts/volksbank/mappings/send-otp.json | 50 + .../mappings/start-psu-authentication.json | 51 + .../payments-sct-get-payment-status.json | 34 + .../mappings/payments-sct-get-sca-status.json | 34 + .../payments-sct-initiate-payment.json | 62 + .../payments-sct-select-sca-method.json | 46 + .../mappings/payments-sct-send-otp.json | 49 + ...ayments-sct-update-psu-authentication.json | 49 + .../__files/body-v1-accounts-1828828.json | 1 + .../__files/body-v1-accounts-34849.json | 1 + .../__files/body-v1-consents-182893.json | 1 + .../__files/body-v1-consents-182939.json | 1 + .../__files/body-v1-consents-55858.json | 1 + .../__files/body-v1-consents-599403.json | 1 + .../mappings/mapping-v1-accounts-1828828.json | 32 + .../mappings/mapping-v1-accounts-34849.json | 32 + .../mappings/mapping-v1-consents-182893.json | 41 + .../mappings/mapping-v1-consents-182939.json | 41 + .../mappings/mapping-v1-consents-55858.json | 41 + .../mappings/mapping-v1-consents-599403.json | 45 + ...-payments-sepa-credit-transfers-CDRF6.json | 1 + ...-payments-sepa-credit-transfers-DNg0h.json | 1 + ...-payments-sepa-credit-transfers-Qjgvn.json | 1 + ...-payments-sepa-credit-transfers-S8p45.json | 1 + ...-payments-sepa-credit-transfers-Y6VfP.json | 1 + ...1-payments-sepa-credit-transfers-4556.json | 26 + ...-payments-sepa-credit-transfers-CDRF6.json | 38 + ...-payments-sepa-credit-transfers-DNg0h.json | 38 + ...-payments-sepa-credit-transfers-Qjgvn.json | 38 + ...-payments-sepa-credit-transfers-S8p45.json | 26 + ...-payments-sepa-credit-transfers-Y6VfP.json | 42 + .../select-sca-method-response-body.json | 17 + ...tart-psu-authentication-response-body.json | 18 + .../dkb/mappings/create-allPsd2-consent.json | 56 + .../dkb/mappings/create-consent-error.json | 56 + .../accounts/dkb/mappings/create-consent.json | 56 + .../accounts/dkb/mappings/get-accounts.json | 41 + .../dkb/mappings/get-consent-status.json | 44 + .../accounts/dkb/mappings/oauth-token.json | 42 + .../dkb/mappings/select-sca-method.json | 47 + .../accounts/dkb/mappings/send-otp.json | 47 + .../mappings/start-psu-authentication.json | 47 + .../initiate-payment-response-body.json | 15 + .../select-sca-method-response-body.json | 17 + ...tart-psu-authentication-response-body.json | 18 + .../dkb/mappings/get-payment-status.json | 32 + .../dkb/mappings/initiate-payment.json | 48 + .../payments/dkb/mappings/pre-aus-token.json | 41 + .../dkb/mappings/select-sca-method.json | 46 + .../payments/dkb/mappings/send-otp.json | 46 + .../mappings/start-psu-authentication.json | 47 + .../mappings/ais-create-consent.json | 53 + .../sparkasse/mappings/ais-get-accounts.json | 35 + .../mappings/ais-get-transactions.json | 35 + .../mappings/ais-select-sca-method.json | 39 + .../sparkasse/mappings/ais-send-otp.json | 38 + .../ais-start-psu-authentication.json | 36 + .../mappings/update-psu-authentication.json | 40 + .../mappings/mapping-v1-consents-14841.json | 4 +- .../__files/body-account-response.json | 1 + .../__files/create-consent-body.json | 1 + .../santander/mappings/create-consent.json | 39 + .../santander/mappings/get-access-token.json | 24 + .../santander/mappings/get-accounts.json | 32 + .../get-oauth-authorization-endpoint.json | 19 + .../santander/mappings/get-oauth-token.json | 31 + .../mappings/mapping-v1-27380.json | 4 +- .../__files/body-payment-status-response.json | 1 + .../__files/created-payment-body.json | 18 + .../santander/mappings/create-payment.json | 39 + .../santander/mappings/get-access-token.json | 24 + .../get-oauth-authorization-endpoint.json | 19 + .../santander/mappings/get-oauth-token.json | 31 + .../mappings/get-payment-status.json | 26 + .../mappings/mapping-v1-consents-47431.json | 4 +- .../mappings/mapping-v1-consents-55757.json | 4 +- .../results-xs2a/__files/body-v1-11840.json | 2 +- .../mappings/mapping-v1-11840.json | 4 +- .../mappings/mapping-v1-58960.json | 4 +- .../__files/body-account-response.json | 1 + .../__files/create-consent-body.json | 1 + .../consorsbank/mappings/create-consent.json | 44 + .../consorsbank/mappings/get-accounts.json | 29 + .../mappings/mapping-v1-consents-24281.json | 4 +- .../mappings/mapping-v1-consents-24283.json | 4 +- .../mappings/mapping-v1-consents-3.json | 4 +- .../mappings/mapping-v1-consents-4.json | 4 +- ...y-acsp-payments-sepa-credit-transfers.json | 1 + ...s-sepa-credit-transfers-authorization.json | 1 + ...y-rcvd-payments-sepa-credit-transfers.json | 1 + .../mappings/get-payments-authorization.json | 37 + .../mappings/get-payments-status.json | 28 + .../mappings/initiate-payments.json | 39 + .../mappings/mapping-v1-consents-8071.json | 4 +- .../mappings/mapping-v1-consents-9071.json | 4 +- .../shorten-filenames-helper.groovy | 14 +- .../xs2a/tests/e2e/wiremock/Const.java | 2 + ...ckAnonymousConsentE2EXs2aProtocolTest.java | 402 + ...ckAnonymousPaymentE2EXs2aProtocolTest.java | 137 + ...thenticatedPaymentE2EXs2aProtocolTest.java | 136 +- .../WiremockConsentE2EXs2aProtocolTest.java | 307 +- ...onsentNonHappyPathE2EXs2aProtocolTest.java | 2 +- ...erAnonymousPaymentE2EXs2aProtocolTest.java | 78 + ...teBankAfterConsentE2EXs2aProtocolTest.java | 78 + .../xs2a-stress-test/pom.xml | 2 +- opba-protocols/xs2a-protocol/pom.xml | 7 +- .../config/aspspmessages/AspspMessages.java | 17 +- .../protocol/ProtocolUrlsConfiguration.java | 6 + .../xs2aadapter/Xs2aAdapterConfiguration.java | 71 +- .../protocol/xs2a/constant/GlobalConst.java | 11 + .../protocol/xs2a/context/Xs2aContext.java | 54 +- .../xs2a/context/Xs2aResultCache.java | 18 + .../ais/TransactionListXs2aContext.java | 10 + .../xs2a/domain/dto/forms/ScaMethod.java | 4 +- .../entrypoint/AccountStatementMapper.java | 87 + .../xs2a/entrypoint/Xs2aOutcomeMapper.java | 8 +- .../entrypoint/Xs2aResultBodyExtractor.java | 76 +- .../ais/ConsentContextLoadingService.java | 54 + .../entrypoint/ais/Xs2aDeleteConsent.java | 52 + .../ais/Xs2aListAccountsEntrypoint.java | 17 +- .../ais/Xs2aListTransactionsEntrypoint.java | 19 +- .../Xs2aGetAuthorizationState.java | 5 +- .../Xs2aUpdateAuthorization.java | 12 +- .../AuthorizationContinuationService.java | 16 +- .../parsers/XmlTransactionsParser.java | 36 + .../pis/Xs2aGetPaymentInfoEntrypoint.java | 15 +- .../pis/Xs2aGetPaymentStatusEntrypoint.java | 17 +- .../Xs2aInitiateSinglePaymentEntrypoint.java | 2 +- .../service/Xs2aCachedResultAccessor.java | 17 + .../service/protocol/Xs2aFillBpmnContext.java | 5 + .../Xs2aUpdateToRealModeBpmnContext.java | 8 + .../Xs2aExecutionErrorHandler.java | 1 + .../validation/Xs2aReportValidationError.java | 7 + .../Xs2aRestorePreValidationContext.java | 8 + .../Xs2aStorePreValidationContext.java | 7 + .../service/xs2a/Xs2aRedirectExecutor.java | 2 - .../xs2a/ais/Xs2aAccountListingService.java | 56 +- .../xs2a/ais/Xs2aConsentErrorHandler.java | 73 +- .../ais/Xs2aTransactionListingService.java | 101 +- .../service/xs2a/authenticate/ScaUtil.java | 65 + .../StartAuthorizationHandlerUtil.java | 39 + .../StartConsentAuthorization.java | 54 +- .../StartConsentAuthorizationWithPin.java | 160 + .../StartPaymentAuthorization.java | 62 +- .../StartPaymentAuthorizationWithPin.java | 162 + .../authenticate/decoupled/DecoupledUtil.java | 41 + ...aAisCheckDecoupledAuthorisationStatus.java | 67 + .../Xs2aAskForDecoupledFinalization.java | 42 + ...aPisCheckDecoupledAuthorisationStatus.java | 74 + .../AuthorizationPossibleErrorHandler.java | 7 +- ...isAuthenticateConsentWithScaChallenge.java | 23 +- ...Xs2aAisAuthenticateUserConsentWithPin.java | 26 +- .../Xs2aAisReportSelectedScaMethod.java | 26 +- .../authenticate/embedded/Xs2aAskForIban.java | 6 + .../embedded/Xs2aAskForPassword.java | 6 + .../embedded/Xs2aAskForScaChallenge.java | 20 +- .../embedded/Xs2aAskForSelectedScaMethod.java | 6 + ...isAuthenticatePaymentWithScaChallenge.java | 29 +- ...Xs2aPisAuthenticateUserConsentWithPin.java | 34 +- .../Xs2aPisReportSelectedScaMethod.java | 32 +- ...2aDoScaRedirectToAspspForScaChallenge.java | 14 +- ...rectToAspspForScaChallengeAfterCreate.java | 24 + .../service/xs2a/consent/AbortConsent.java | 3 +- .../consent/AisConsentInitiateExtractor.java | 13 +- .../xs2a/consent/AspspConsentDrop.java | 9 +- .../consent/BaseCreateAisConsentService.java | 56 + .../service/xs2a/consent/ConsentFinder.java | 2 +- .../CreateAisAccountListConsentService.java | 32 +- .../xs2a/consent/CreateAisConsentService.java | 38 +- ...reateAisTransactionListConsentService.java | 31 +- ...eConsentOrPaymentPossibleErrorHandler.java | 46 +- .../ReportConsentAuthorizationDenied.java | 4 + .../ReportConsentAuthorizationFinished.java | 4 + .../service/xs2a/consent/Xs2aConsentInfo.java | 83 +- .../Xs2aLoadConsentAndContextFromDb.java | 33 +- .../consent/Xs2aPersistConsentAndContext.java | 10 + .../service/xs2a/dto/ResponseTokenMapper.java | 2 +- .../xs2a/service/xs2a/dto/WithBasicInfo.java | 10 +- .../dto/Xs2aAuthorizedConsentParameters.java | 5 + .../dto/Xs2aInitialConsentParameters.java | 5 + .../dto/Xs2aInitialPaymentParameters.java | 8 +- .../service/xs2a/dto/Xs2aStandardHeaders.java | 12 +- .../xs2a/dto/Xs2aTransactionParameters.java | 28 +- .../xs2a/dto/Xs2aWithBalanceParameters.java | 2 +- .../xs2a/dto/Xs2aWithConsentIdHeaders.java | 4 +- .../ProvidePsuIdAndPsuPasswordBody.java | 34 + .../embedded/ProvidePsuPasswordBody.java | 19 +- .../ProvideScaChallengeResultBody.java | 2 +- .../embedded/SelectScaChallengeBody.java | 2 +- .../dto/consent/AisConsentInitiateBody.java | 12 +- .../dto/consent/ConsentInitiateHeaders.java | 10 +- .../consent/ConsentInitiateParameters.java | 27 + .../xs2a/dto/oauth2/Xs2aOauth2Parameters.java | 6 +- .../oauth2/Xs2aOauth2WithCodeParameters.java | 6 +- .../xs2a/dto/payment/PaymentInfoHeaders.java | 2 +- .../dto/payment/PaymentInfoParameters.java | 2 +- .../xs2a/dto/payment/PaymentInitiateBody.java | 8 +- .../xs2a/dto/payment/PaymentStateHeaders.java | 2 +- .../dto/payment/PaymentStateParameters.java | 2 +- .../xs2a/service/xs2a/oauth2/OAuth2Util.java | 24 + .../oauth2/Xs2aEmbeddedPreAuthorization.java | 112 + .../oauth2/Xs2aOauth2ExchangeCodeToToken.java | 15 +- ...directUserToOauth2AuthorizationServer.java | 32 +- .../payment/CreateSinglePaymentService.java | 80 +- .../ReportPaymentAuthorizationDenied.java | 4 + .../ReportPaymentAuthorizationFinished.java | 4 + .../Xs2aPisPersistPaymentAndContext.java | 6 +- .../xs2a/service/xs2a/quirks/QuirkUtil.java | 4 +- .../AccountAccessBodyValidator.java | 10 +- .../xs2a/validation/Xs2aValidator.java | 5 +- .../util/logresolver/Xs2aLogResolver.java | 286 + .../domain/ValidatedPathHeadersBodyLog.java | 22 + .../domain/ValidatedPathHeadersLog.java | 20 + .../domain/ValidatedPathQueryHeadersLog.java | 22 + .../domain/ValidatedQueryHeadersLog.java | 19 + .../logresolver/domain/Xs2aExecutionLog.java | 14 + .../logresolver/domain/common/PsuDataLog.java | 14 + .../SelectPsuAuthenticationMethodLog.java | 17 + .../common/StartScaprocessResponseLog.java | 7 + .../common/TransactionAuthorisationLog.java | 17 + .../common/UpdatePsuAuthenticationLog.java | 17 + .../domain/common/Xs2aOauth2HeadersLog.java | 17 + .../common/Xs2aOauth2ParametersLog.java | 18 + .../Xs2aOauth2WithCodeParametersLog.java | 18 + .../common/Xs2aResourceParametersLog.java | 18 + .../domain/common/Xs2aStandardHeadersLog.java | 17 + .../common/Xs2aTransactionParametersLog.java | 22 + .../common/Xs2aWithBalanceParametersLog.java | 18 + .../common/Xs2aWithConsentIdHeadersLog.java | 19 + .../domain/consent/AccountAccessLog.java | 22 + .../domain/consent/AccountReferenceLog.java | 16 + .../consent/ConsentInitiateHeadersLog.java | 25 + .../consent/ConsentInitiateParametersLog.java | 15 + .../domain/consent/ConsentsLog.java | 23 + .../Xs2aAuthorizedConsentParametersLog.java | 18 + .../Xs2aInitialConsentParametersLog.java | 16 + .../context/AuthenticationObjectLog.java | 16 + .../domain/context/BaseContextLog.java | 23 + .../domain/context/ChallengeDataLog.java | 18 + .../domain/context/ServiceContextLog.java | 26 + .../context/StartScaprocessResponseLog.java | 22 + .../TransactionListXs2aContextLog.java | 20 + .../domain/context/Xs2aContextLog.java | 50 + .../domain/context/Xs2aPisContextLog.java | 17 + .../domain/payment/AddressLog.java | 16 + .../logresolver/domain/payment/AmountLog.java | 12 + .../payment/PaymentInitiateHeadersLog.java | 16 + .../payment/PaymentInitiationJsonLog.java | 26 + .../Xs2aAuthorizedPaymentParametersLog.java | 18 + .../Xs2aInitialPaymentParametersLog.java | 20 + ...tartPaymentAuthorizationParametersLog.java | 16 + .../domain/response/ResponseLog.java | 37 + .../domain/response/TokenResponseLog.java | 22 + .../logresolver/domain/response/URILog.java | 34 + .../mapper/Xs2aDtoToLogObjectsMapper.java | 229 + .../main/resources/adapter.config.properties | 2 - .../src/main/resources/application.yml | 18 +- .../accounts/xs2a-list-accounts.bpmn20.xml | 6 +- .../consent/xs2a-authorize-consent.bpmn20.xml | 322 +- .../oauth2/xs2a-oauth2-step.bpmn20.xml | 84 +- .../payment/xs2a-authorize-payment.bpmn20.xml | 318 +- .../xs2a/config/MapperTestConfig.java | 1 + .../forms/ScaMethodFromAuthObjectTest.java | 2 +- .../entrypoint/Xs2AToFacadeMapperTest.java | 100 +- ...ListAccountsEntrypointFromRequestTest.java | 24 + ...TransactionsEntrypointFromRequestTest.java | 24 + .../service/ContextUpdateServiceTest.java | 2 +- ...eConsentWithScaChallengeClearsScaTest.java | 12 +- ...eWithPinUserConsentClearsPasswordTest.java | 10 +- .../ProvidePsuPasswordBodyToXs2aApiTest.java | 2 +- ...deScaChallengeResultBodyToXs2aApiTest.java | 2 +- .../SelectScaChallengeBodyToXs2aApiTest.java | 2 +- .../AisConsentInitiateBodyToXs2aApiTest.java | 20 +- ...2aOauth2WithCodeParametersFromCtxTest.java | 52 + ..._from_ctx_authorization_request_input.json | 2 +- ...t_from_ctx_list_account_request_input.json | 5 +- ..._from_ctx_list_account_request_output.json | 5 +- ...ist_account_request_with_extras_input.json | 28 + ...st_account_request_with_extras_output.json | 19 + ...hod_from_auth_object_sca_method_input.json | 2 +- ...od_from_auth_object_sca_method_output.json | 4 +- ..._from_ctx_authorization_request_input.json | 2 +- ..._from_list_transactions_request_input.json | 5 +- ...from_list_transactions_request_output.json | 3 +- ...ransactions_request_with_extras_input.json | 34 + ...ansactions_request_with_extras_output.json | 41 + ...de_parameters_from_xs2a_context_input.json | 41 + ...e_parameters_from_xs2a_context_output.json | 5 + ...rom_xs2a_context_to_parameters_output.json | 5 + ...facade_response_mapper_accounts_input.json | 7 +- ...acade_response_mapper_accounts_output.json | 2 - ...apper_list_transactions_request_input.json | 34 + ...ransactions_request_with_paging_input.json | 36 + ...de_response_mapper_transactions_input.json | 36 +- ...e_response_mapper_transactions_output.json | 3 +- ...mapper_transactions_with_paging_input.json | 226 + ...apper_transactions_with_paging_output.json | 191 + .../camt-multibanking.xml | 241 + .../camt-sparkasse.xml | 186 + .../multibanking-output.json | 171 + .../sparkasse-output.json | 84 + opba-protocols/xs2a-sandbox-protocol/pom.xml | 60 +- .../NoResponseXs2aAccountListingService.java | 7 +- .../SandboxXs2aTransactionListingService.java | 20 +- opba-rest-api-shared/pom.xml | 2 +- .../opba/restapi/shared/HttpHeaders.java | 2 + .../shared/service/FacadeResponseMapper.java | 2 + .../service/RedirectionOnlyToOkMapper.java | 7 +- pom.xml | 49 +- ...HBCI-2-SCA-METHODS.postman_collection.json | 1766 + ...DDED-2-SCA-METHODS.postman_collection.json | 1192 + ...A-METHODS-NO-LOGIN.postman_collection.json | 875 + .../OPBA-DEV-NO-SIG.postman_environment.json | 99 + .../OPBA-DEV.postman_environment.json | 99 + .../OPBA-LOCAL.postman_environment.json | 99 + scripts/build_firefly_exporter_mvn.sh | 10 +- scripts/build_mvn.sh | 8 +- scripts/deploy_javadoc.sh | 2 +- scripts/deploy_mvn.sh | 4 +- scripts/deploy_mvn_release_candidate.sh | 2 +- scripts/deploy_openshift.sh | 8 +- scripts/install_mvn.sh | 2 +- scripts/mvn_deploy_check_develop.sh | 2 +- scripts/upload_coverage_to_codecov.sh | 21 - smoke-tests/pom.xml | 2 +- .../OpbaApiWithHbciConsentUiSmokeE2ETest.java | 19 +- .../test/resources/application-test-smoke.yml | 2 +- ui-test-scripts/sca-oauth2.http | 4 +- 969 files changed, 126027 insertions(+), 82563 deletions(-) create mode 100644 .env create mode 100644 .github/workflows/deploy-to-integ.yml create mode 100644 .github/workflows/manual-heavy-tests.yml delete mode 100644 consent-ui/src/app/ais/common/account-details/account-details.component.html delete mode 100644 consent-ui/src/app/ais/common/account-details/account-details.component.spec.ts delete mode 100644 consent-ui/src/app/ais/common/account-details/account-details.component.ts create mode 100644 consent-ui/src/app/ais/common/constant/constant.ts create mode 100644 consent-ui/src/app/ais/common/date-util.ts create mode 100644 consent-ui/src/app/ais/wait-for-decoupled/wait-for-decoupled.html rename consent-ui/src/app/ais/{common/account-details/account-details.component.scss => wait-for-decoupled/wait-for-decoupled.scss} (100%) create mode 100644 consent-ui/src/app/ais/wait-for-decoupled/wait-for-decoupled.ts delete mode 100644 consent-ui/src/app/api/api/consentAuthorization.service.ts delete mode 100644 consent-ui/src/app/api/model/authorizeRequest.ts delete mode 100644 consent-ui/src/app/api/model/consentAuthRequiredField.ts delete mode 100644 consent-ui/src/app/api/model/consentAuthRequirement.ts delete mode 100644 consent-ui/src/app/api/model/denyRedirectRequest.ts delete mode 100644 consent-ui/src/app/api/model/denyRequest.ts delete mode 100644 consent-ui/src/app/api/model/inlineResponse200.ts create mode 100644 consent-ui/src/app/pis/wait-for-decoupled/wait-for-decoupled.html create mode 100644 consent-ui/src/app/pis/wait-for-decoupled/wait-for-decoupled.scss create mode 100644 consent-ui/src/app/pis/wait-for-decoupled/wait-for-decoupled.ts create mode 100644 docs/img/big-picture.png rename fintech-examples/fintech-server/src/test/resources/{TPP_BankProfileResponse-aaaaaaaaa-ee6e-45f9-9163-b97320c6881a.json => TPP_BankProfileResponse-356938ab-9561-408f-ac7a-a9089c1623b7.json} (100%) create mode 100644 fintech-examples/fintech-ui/src/app/api/api/fintechRetrieveConsent.service.ts create mode 100644 fintech-examples/fintech-ui/src/app/api/model/aisAccountAccessInfo.ts create mode 100644 fintech-examples/fintech-ui/src/app/api/model/aisConsentRequest.ts create mode 100644 fintech-examples/fintech-ui/src/app/api/model/analyticsReportDetails.ts create mode 100644 helpers/docker-wiremock.yml create mode 100644 opba-analytics/opba-analytics-api/pom.xml create mode 100644 opba-analytics/opba-analytics-api/src/main/java/de/adorsys/opba/analytics/GlobalConst.java create mode 100644 opba-analytics/opba-analytics-api/src/main/java/de/adorsys/opba/analytics/TransactionAnalyzer.java create mode 100644 opba-analytics/opba-analytics-api/src/main/java/de/adorsys/opba/analytics/dto/AnalyticsRequest.java create mode 100644 opba-analytics/pom.xml create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/dto/PubAndPrivKey.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/services/FacadeOptionalService.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/services/GetAuthorizationStatusService.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/services/ais/DeleteConsentService.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/services/ais/GetAisAuthorizationStatusService.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/services/ais/UpdateExternalAisSessionService.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/services/authorization/OnLoginService.java delete mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/services/authorization/PsuLoginForAisService.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/services/authorization/PsuLoginService.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/services/pis/GetPisAuthorizationStatusService.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/services/scoped/consentaccess/AnonymousPsuConsentAccess.java rename opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/services/scoped/consentaccess/{FintechConsentAccess.java => FintechConsentAccessImpl.java} (63%) create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/util/logresolver/FacadeLogResolver.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/util/logresolver/domain/ActionLog.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/util/logresolver/domain/ProtocolWithCtxLog.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/util/logresolver/domain/context/ServiceContextLog.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/util/logresolver/domain/request/FacadeServiceableRequestLog.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/util/logresolver/domain/request/RequestLog.java create mode 100644 opba-banking-protocol-facade/src/main/java/de/adorsys/opba/protocol/facade/util/logresolver/domain/response/ResultLog.java create mode 100644 opba-banking-rest-impl/src/main/java/de/adorsys/opba/tppbankingapi/controller/UuidMapper.java create mode 100644 opba-banking-rest-impl/src/test/resources/application-test-search.yml create mode 100644 opba-consent-rest-impl/src/test/java/de/adorsys/opba/consentapi/service/mapper/AisConsentMapperTest.java create mode 100644 opba-db/mock-data-generate.groovy create mode 100644 opba-db/src/main/java/de/adorsys/opba/db/domain/converter/ResultContentTypeConverter.java create mode 100644 opba-db/src/main/java/de/adorsys/opba/db/domain/converter/SupportedConsentTypeConverter.java create mode 100644 opba-db/src/main/java/de/adorsys/opba/db/domain/entity/helpers/UuidMapper.java create mode 100644 opba-db/src/main/resources/migration/migrations/0004-add-skip-auth-for-deutsche-bank.xml create mode 100644 opba-db/src/main/resources/migration/migrations/0005-enable-finapi-test-bank.xml create mode 100644 opba-db/src/main/resources/migration/migrations/0009-add-consent-cache.xml create mode 100644 opba-db/src/main/resources/migration/migrations/0010-update-dkb-adapter.xml create mode 100644 opba-db/src/main/resources/migration/migrations/0011-add-validation-ignore-decoupled.xml create mode 100644 opba-db/src/main/resources/migration/migrations/0012-add-is-active-field-to-bank.xml create mode 100644 opba-db/src/main/resources/migration/migrations/0013-add-auth-session-status.xml create mode 100644 opba-db/src/main/resources/migration/migrations/0014-update-sparkasse-adapter.xml create mode 100644 opba-db/src/main/resources/migration/migrations/0017-add-consent-type-support.xml create mode 100644 opba-db/src/main/resources/migration/migrations/0018-add-skip-psu-auth-for-verlag-bank.xml create mode 100644 opba-db/src/main/resources/migration/migrations/0019-set-start-consent-auth-with-pin-fiducia.xml delete mode 100644 opba-db/src/main/resources/migration/migrations/bank_action_data.csv delete mode 100644 opba-db/src/main/resources/migration/migrations/bank_profile_data.csv delete mode 100644 opba-db/src/main/resources/migration/migrations/bank_sub_action_data.csv create mode 100644 opba-db/src/main/resources/migration/migrations/csv/v0/0-banks.csv create mode 100644 opba-db/src/main/resources/migration/migrations/csv/v0/0-hbci_bank-action.csv create mode 100644 opba-db/src/main/resources/migration/migrations/csv/v0/0-hbci_bank-sub-action.csv create mode 100644 opba-db/src/main/resources/migration/migrations/csv/v0/0-hbci_bank_bank-profile.csv create mode 100644 opba-db/src/main/resources/migration/migrations/csv/v0/0-mock-banks.csv create mode 100644 opba-db/src/main/resources/migration/migrations/csv/v0/0-mock_bank_profile.csv create mode 100644 opba-db/src/main/resources/migration/migrations/csv/v0/0-xs2a_bank-action.csv create mode 100644 opba-db/src/main/resources/migration/migrations/csv/v0/0-xs2a_bank-profile.csv create mode 100644 opba-db/src/main/resources/migration/migrations/csv/v0/0-xs2a_bank-sub-action.csv rename opba-db/src/main/resources/migration/migrations/{ => csv/v0}/banks_random_data.csv (100%) rename opba-db/src/main/resources/migration/migrations/{banks.csv => csv/v0/xs2a-adapter-banks.csv} (67%) delete mode 100644 opba-embedded-starter-tests/src/test/resources/adminapi/918d80fa-f7fd-4c9f-a6bd-7a9e12aeee76-updated.json create mode 100644 opba-embedded-starter-tests/src/test/resources/adminapi/adadadad-1000-0000-0000-b0b0b0b0b0b0-profiles-removed.json create mode 100644 opba-embedded-starter-tests/src/test/resources/adminapi/adadadad-1000-0000-0000-b0b0b0b0b0b0-profiles-replaced.json create mode 100644 opba-embedded-starter-tests/src/test/resources/adminapi/adadadad-1000-0000-0000-b0b0b0b0b0b0-updated.json rename opba-embedded-starter-tests/src/test/resources/adminapi/{918d80fa-f7fd-4c9f-a6bd-7a9e12aeee76.json => adadadad-1000-0000-0000-b0b0b0b0b0b0.json} (56%) create mode 100644 opba-embedded-starter-tests/src/test/resources/adminapi/adadadad-4000-0000-0000-b0b0b0b0b0b0-profiles-replaced.json create mode 100644 opba-embedded-starter-tests/src/test/resources/application-test-separate-db.yml create mode 100644 opba-embedded-starter/src/main/java/de/adorsys/opba/starter/config/extensions/OpbaFinapiExtensionConfig.java create mode 100644 opba-embedded-starter/src/main/java/de/adorsys/opba/starter/config/extensions/OpbaSmartAnalyticsExtensionConfig.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/Constants.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/common/ResultContentType.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/common/SessionStatus.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/common/SupportedConsentType.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/NotSensitiveData.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/request/accounts/AisAuthorizationStatusRequest.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/request/accounts/UpdateExternalAisSessionRequest.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/request/authorization/DeleteConsentRequest.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/request/authorization/OnLoginRequest.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/request/payments/PisAuthorizationStatusRequest.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/result/body/AisAuthorizationStatusBody.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/result/body/AnalyticsResult.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/result/body/AuthorizationStatusBody.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/result/body/DeleteConsentBody.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/result/body/DetailedSessionStatus.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/result/body/Paging.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/result/body/PisAuthorizationStatusBody.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/dto/result/body/UpdateExternalAisSessionBody.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/services/ResultBodyPostProcessor.java create mode 100644 opba-facade-protocol-api-shared/src/main/java/de/adorsys/opba/protocol/api/services/scoped/consent/FintechConsentAccess.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/config/HbciScaConfiguration.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/entrypoint/ais/HbciDeleteConsent.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/util/logresolver/HbciLogResolver.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/util/logresolver/domain/AccountListHbciContextLog.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/util/logresolver/domain/BaseContextLog.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/util/logresolver/domain/HbciContextLog.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/util/logresolver/domain/HbciExecutionLog.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/util/logresolver/domain/PaymentHbciContextLog.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/util/logresolver/domain/TransactionListHbciContextLog.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/util/logresolver/domain/request/RequestLog.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/util/logresolver/domain/request/TransactionRequestLog.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/util/logresolver/domain/response/ResponseLog.java create mode 100644 opba-protocols/hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci/util/logresolver/mapper/HbciDtoToLogObjectsMapper.java create mode 100644 opba-protocols/hbci-protocol/src/test/java/de/adorsys/opba/protocol/hbci/entrypoint/authorization/HbciUpdateAuthorizationTest.java create mode 100644 opba-protocols/hbci-protocol/src/test/resources/application-test.yml create mode 100644 opba-protocols/opba-protocol-api/src/main/java/de/adorsys/opba/protocol/api/ais/DeleteConsent.java create mode 100644 opba-protocols/opba-protocol-api/src/main/java/de/adorsys/opba/protocol/api/ais/GetAisAuthorizationStatus.java create mode 100644 opba-protocols/opba-protocol-api/src/main/java/de/adorsys/opba/protocol/api/ais/UpdateExternalAisSession.java create mode 100644 opba-protocols/opba-protocol-api/src/main/java/de/adorsys/opba/protocol/api/authorization/OnLogin.java create mode 100644 opba-protocols/opba-protocol-api/src/main/java/de/adorsys/opba/protocol/api/errors/ProcessErrorConsentGone.java delete mode 100644 opba-protocols/opba-protocol-api/src/main/java/de/adorsys/opba/protocol/api/errors/ProcessErrorStrings.java create mode 100644 opba-protocols/opba-protocol-api/src/main/java/de/adorsys/opba/protocol/api/errors/ReturnableException.java create mode 100644 opba-protocols/opba-protocol-api/src/main/java/de/adorsys/opba/protocol/api/pis/GetPisAuthorizationStatus.java create mode 100644 opba-protocols/opba-protocol-testing-helper/Dockerfile create mode 100644 opba-protocols/opba-protocol-testing-helper/README.md create mode 100644 opba-protocols/opba-protocol-testing-helper/pom.xml create mode 100644 opba-protocols/opba-protocol-testing-helper/postman/OBG-HELPER-XS2A-EMBEDDED.postman_collection.json create mode 100644 opba-protocols/opba-protocol-testing-helper/src/main/java/de/adorsys/opba/helpers/protocol/testing/OpenBankingTestingHelperApplication.java rename {opba-embedded-starter/src/main/java/de/adorsys/opba/starter => opba-protocols/opba-protocol-testing-helper/src/main/java/de/adorsys/opba/helpers/protocol/testing}/config/DevProfile.java (91%) create mode 100644 opba-protocols/opba-protocol-testing-helper/src/main/java/de/adorsys/opba/helpers/protocol/testing/config/WebConfig.java create mode 100644 opba-protocols/opba-protocol-testing-helper/src/main/java/de/adorsys/opba/helpers/protocol/testing/controller/ProtocolTestingController.java create mode 100644 opba-protocols/opba-protocol-testing-helper/src/main/java/de/adorsys/opba/helpers/protocol/testing/service/MapBasedAspspRepository.java create mode 100644 opba-protocols/opba-protocol-testing-helper/src/main/java/de/adorsys/opba/helpers/protocol/testing/service/MapBasedRequestScopedServicesProvider.java create mode 100644 opba-protocols/opba-protocol-testing-helper/src/main/resources/adapter.config.properties create mode 100644 opba-protocols/opba-protocol-testing-helper/src/main/resources/application.yml create mode 100644 opba-protocols/opba-protocol-testing-helper/src/main/resources/logback.xml create mode 100644 opba-protocols/opba-protocol-testing-helper/src/main/resources/sample-qwac.keystore create mode 100644 opba-protocols/opba-protocol-testing-helper/suppressions.xml create mode 100644 opba-protocols/protocol-bpmn-shared/src/main/java/de/adorsys/opba/protocol/bpmnshared/dto/context/ProtocolResultCache.java create mode 100644 opba-protocols/protocol-bpmn-shared/src/main/java/de/adorsys/opba/protocol/bpmnshared/dto/messages/InternalReturnableConsentGoneProcessError.java delete mode 100644 opba-protocols/protocol-bpmn-shared/src/main/java/de/adorsys/opba/protocol/bpmnshared/dto/messages/InternalReturnableProcessError.java rename opba-protocols/{hbci-protocol/src/main/java/de/adorsys/opba/protocol/hbci => protocol-bpmn-shared/src/main/java/de/adorsys/opba/protocol/bpmnshared}/service/SafeCacheSerDeUtil.java (93%) create mode 100644 opba-protocols/protocol-bpmn-shared/src/main/java/de/adorsys/opba/protocol/bpmnshared/service/TransactionUtil.java create mode 100644 opba-protocols/protocol-bpmn-shared/src/main/java/de/adorsys/opba/protocol/bpmnshared/service/cache/CachedResultAccessor.java create mode 100644 opba-protocols/protocol-bpmn-shared/src/main/java/de/adorsys/opba/protocol/bpmnshared/util/logResolver/LogResolver.java create mode 100644 opba-protocols/protocol-bpmn-shared/src/main/java/de/adorsys/opba/protocol/bpmnshared/util/logResolver/domain/ContextLog.java create mode 100644 opba-protocols/protocol-bpmn-shared/src/main/java/de/adorsys/opba/protocol/bpmnshared/util/logResolver/domain/ExecutionLog.java create mode 100644 opba-protocols/protocol-bpmn-shared/src/main/java/de/adorsys/opba/protocol/bpmnshared/util/logResolver/mapper/DtoToLogObjectsMapper.java create mode 100644 opba-protocols/sandboxes/hbci-sandbox/src/main/java/de/adorsys/opba/protocol/sandbox/hbci/util/logresolver/HbciSandboxLogResolver.java create mode 100644 opba-protocols/sandboxes/hbci-sandbox/src/main/java/de/adorsys/opba/protocol/sandbox/hbci/util/logresolver/domain/HbciSandboxContextLog.java create mode 100644 opba-protocols/sandboxes/hbci-sandbox/src/main/java/de/adorsys/opba/protocol/sandbox/hbci/util/logresolver/domain/HbciSandboxExecutionLog.java create mode 100644 opba-protocols/sandboxes/hbci-sandbox/src/main/java/de/adorsys/opba/protocol/sandbox/hbci/util/logresolver/mapper/HbciSandboxDtoToLogObjectsMapper.java create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-sandbox/src/test/java/de/adorsys/opba/protocol/xs2a/tests/e2e/sandbox/SandboxE2EProtocolAisOauth2Test.java create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-tests-common/src/main/java/de/adorsys/opba/protocol/xs2a/tests/e2e/stages/AdminUtil.java create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-tests-common/src/main/java/de/adorsys/opba/protocol/xs2a/tests/e2e/stages/RequestStatusUtil.java create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-tests-common/src/main/resources/restrecord/tpp-ui-input/params/anton-brueckner-transactions-no-consent.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-tests-common/src/main/resources/restrecord/tpp-ui-input/params/max-musterman-dedicated-account-consent.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/__files/body-v1-accounts-55313.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/__files/body-v1-consents-2059236999.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/__files/body-v1-consents-3192330705.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/__files/body-v1-consents-3258146603.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/__files/body-v1-consents-3496770420.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/__files/body-v1-consents-370291184.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/mappings/mapping-v1-accounts-55313.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/mappings/mapping-v1-consents-2059235614.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/mappings/mapping-v1-consents-2059235615.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/mappings/mapping-v1-consents-2059236656.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/mappings/mapping-v1-consents-3192330705.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/mappings/mapping-v1-consents-3258146603.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/mappings/mapping-v1-consents-3496770420.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/mappings/mapping-v1-consents-370291184.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/targobank/mappings/create-consent.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/targobank/mappings/get-accounts.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/targobank/mappings/get-consent-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/targobank/mappings/get-sca-finalized-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/targobank/mappings/get-sca-scaMethodSelected-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/accounts/targobank/mappings/update-psu-authentication.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/__files/body-v1-1063823569.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/__files/body-v1-1063823571.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/__files/body-v1-3832098252.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/__files/body-v1-492790064.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/__files/body-v1-916399554.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/__files/body-v1-payments-sepa-credit-transfers-12221.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/mappings/mapping-v1-1063823569.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/mappings/mapping-v1-1063823570.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/mappings/mapping-v1-1063823571.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/mappings/mapping-v1-3832098252.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/mappings/mapping-v1-492790064.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/mappings/mapping-v1-916399554.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/mappings/mapping-v1-payments-sepa-credit-transfers-4556.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/targobank/mappings/payments-sct-get-payment-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/targobank/mappings/payments-sct-get-sca-finalized-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/targobank/mappings/payments-sct-get-sca-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/targobank/mappings/payments-sct-initiate-payment.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/decoupled-mode/payments/targobank/mappings/payments-sct-update-psu-authentication.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/__files/body-v1-accounts-55313.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/__files/body-v1-consents-2059236824.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/__files/body-v1-consents-2059236999.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/__files/body-v1-consents-2145005849.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/__files/body-v1-consents-328937491.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/__files/body-v1-consents-3657373201.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/mappings/mapping-v1-accounts-55313.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/mappings/mapping-v1-consents-2059235614.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/mappings/mapping-v1-consents-2059235615.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/mappings/mapping-v1-consents-2059236656.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/mappings/mapping-v1-consents-2059236824.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/mappings/mapping-v1-consents-2145005849.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/mappings/mapping-v1-consents-328937491.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/accounts/mappings/mapping-v1-consents-3657373201.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/__files/body-v1-1063823569.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/__files/body-v1-1063823571.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/__files/body-v1-3832098252.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/__files/body-v1-492790064.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/__files/body-v1-916399554.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/__files/body-v1-payments-sepa-credit-transfers-12221.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/mappings/mapping-v1-1063823569.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/mappings/mapping-v1-1063823570.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/mappings/mapping-v1-1063823571.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/mappings/mapping-v1-3832098252.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/mappings/mapping-v1-492790064.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/mappings/mapping-v1-916399554.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/decoupled-sca/embedded-mode-decoupled-sca/payments/mappings/mapping-v1-payments-sepa-credit-transfers-4556.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/postbank/mappings/create-consent.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/postbank/mappings/get-accounts-with-balances.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/postbank/mappings/get-accounts.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/postbank/mappings/get-balances.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/postbank/mappings/get-consent-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/postbank/mappings/get-transactions.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/postbank/mappings/select-sca-method.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/postbank/mappings/send-otp.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/postbank/mappings/update-psu-authentication.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/volksbank/__files/response-body-get-accounts.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/volksbank/__files/response-body-get-transactions.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/volksbank/__files/response-body-start-psu-authentication.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/volksbank/mappings/create-consent.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/volksbank/mappings/get-accounts.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/volksbank/mappings/get-transactions.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/volksbank/mappings/select-sca-method.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/volksbank/mappings/send-otp.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/volksbank/mappings/start-psu-authentication.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/payments/postbank/mappings/payments-sct-get-payment-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/payments/postbank/mappings/payments-sct-get-sca-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/payments/postbank/mappings/payments-sct-initiate-payment.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/payments/postbank/mappings/payments-sct-select-sca-method.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/payments/postbank/mappings/payments-sct-send-otp.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/payments/postbank/mappings/payments-sct-update-psu-authentication.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/__files/body-v1-accounts-1828828.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/__files/body-v1-accounts-34849.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/__files/body-v1-consents-182893.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/__files/body-v1-consents-182939.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/__files/body-v1-consents-55858.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/__files/body-v1-consents-599403.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/mappings/mapping-v1-accounts-1828828.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/mappings/mapping-v1-accounts-34849.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/mappings/mapping-v1-consents-182893.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/mappings/mapping-v1-consents-182939.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/mappings/mapping-v1-consents-55858.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/accounts/sandbox/mappings/mapping-v1-consents-599403.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/payments/sandbox/__files/body-v1-payments-sepa-credit-transfers-CDRF6.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/payments/sandbox/__files/body-v1-payments-sepa-credit-transfers-DNg0h.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/payments/sandbox/__files/body-v1-payments-sepa-credit-transfers-Qjgvn.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/payments/sandbox/__files/body-v1-payments-sepa-credit-transfers-S8p45.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/payments/sandbox/__files/body-v1-payments-sepa-credit-transfers-Y6VfP.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/payments/sandbox/mappings/mapping-v1-payments-sepa-credit-transfers-4556.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/payments/sandbox/mappings/mapping-v1-payments-sepa-credit-transfers-CDRF6.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/payments/sandbox/mappings/mapping-v1-payments-sepa-credit-transfers-DNg0h.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/payments/sandbox/mappings/mapping-v1-payments-sepa-credit-transfers-Qjgvn.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/payments/sandbox/mappings/mapping-v1-payments-sepa-credit-transfers-S8p45.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/one-sca/payments/sandbox/mappings/mapping-v1-payments-sepa-credit-transfers-Y6VfP.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/accounts/dkb/__files/select-sca-method-response-body.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/accounts/dkb/__files/start-psu-authentication-response-body.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/accounts/dkb/mappings/create-allPsd2-consent.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/accounts/dkb/mappings/create-consent-error.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/accounts/dkb/mappings/create-consent.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/accounts/dkb/mappings/get-accounts.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/accounts/dkb/mappings/get-consent-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/accounts/dkb/mappings/oauth-token.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/accounts/dkb/mappings/select-sca-method.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/accounts/dkb/mappings/send-otp.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/accounts/dkb/mappings/start-psu-authentication.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/payments/dkb/__files/initiate-payment-response-body.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/payments/dkb/__files/select-sca-method-response-body.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/payments/dkb/__files/start-psu-authentication-response-body.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/payments/dkb/mappings/get-payment-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/payments/dkb/mappings/initiate-payment.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/payments/dkb/mappings/pre-aus-token.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/payments/dkb/mappings/select-sca-method.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/payments/dkb/mappings/send-otp.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/pre-step/payments/dkb/mappings/start-psu-authentication.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/sparkasse/mappings/ais-create-consent.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/sparkasse/mappings/ais-get-accounts.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/sparkasse/mappings/ais-get-transactions.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/sparkasse/mappings/ais-select-sca-method.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/sparkasse/mappings/ais-send-otp.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/sparkasse/mappings/ais-start-psu-authentication.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/sparkasse/mappings/update-psu-authentication.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/accounts/santander/__files/body-account-response.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/accounts/santander/__files/create-consent-body.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/accounts/santander/mappings/create-consent.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/accounts/santander/mappings/get-access-token.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/accounts/santander/mappings/get-accounts.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/accounts/santander/mappings/get-oauth-authorization-endpoint.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/accounts/santander/mappings/get-oauth-token.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/payments/santander/__files/body-payment-status-response.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/payments/santander/__files/created-payment-body.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/payments/santander/mappings/create-payment.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/payments/santander/mappings/get-access-token.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/payments/santander/mappings/get-oauth-authorization-endpoint.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/payments/santander/mappings/get-oauth-token.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/oauth2/integrated/payments/santander/mappings/get-payment-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/redirect/accounts/consorsbank/__files/body-account-response.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/redirect/accounts/consorsbank/__files/create-consent-body.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/redirect/accounts/consorsbank/mappings/create-consent.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/redirect/accounts/consorsbank/mappings/get-accounts.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/redirect/payments/consorsbank/__files/body-acsp-payments-sepa-credit-transfers.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/redirect/payments/consorsbank/__files/body-payments-sepa-credit-transfers-authorization.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/redirect/payments/consorsbank/__files/body-rcvd-payments-sepa-credit-transfers.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/redirect/payments/consorsbank/mappings/get-payments-authorization.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/redirect/payments/consorsbank/mappings/get-payments-status.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/redirect/payments/consorsbank/mappings/initiate-payments.json create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/test/java/de/adorsys/opba/protocol/xs2a/tests/e2e/wiremock/WiremockAnonymousConsentE2EXs2aProtocolTest.java create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/test/java/de/adorsys/opba/protocol/xs2a/tests/e2e/wiremock/WiremockDeleteBankAfterAnonymousPaymentE2EXs2aProtocolTest.java create mode 100644 opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/test/java/de/adorsys/opba/protocol/xs2a/tests/e2e/wiremock/WiremockDeleteBankAfterConsentE2EXs2aProtocolTest.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/context/Xs2aResultCache.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/entrypoint/AccountStatementMapper.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/entrypoint/ais/ConsentContextLoadingService.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/entrypoint/ais/Xs2aDeleteConsent.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/entrypoint/parsers/XmlTransactionsParser.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/Xs2aCachedResultAccessor.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/authenticate/ScaUtil.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/authenticate/StartAuthorizationHandlerUtil.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/authenticate/StartConsentAuthorizationWithPin.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/authenticate/StartPaymentAuthorizationWithPin.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/authenticate/decoupled/DecoupledUtil.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/authenticate/decoupled/Xs2aAisCheckDecoupledAuthorisationStatus.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/authenticate/decoupled/Xs2aAskForDecoupledFinalization.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/authenticate/decoupled/Xs2aPisCheckDecoupledAuthorisationStatus.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/authenticate/redirect/Xs2aDoScaRedirectToAspspForScaChallengeAfterCreate.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/consent/BaseCreateAisConsentService.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/dto/authenticate/embedded/ProvidePsuIdAndPsuPasswordBody.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/dto/consent/ConsentInitiateParameters.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/oauth2/OAuth2Util.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/service/xs2a/oauth2/Xs2aEmbeddedPreAuthorization.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/Xs2aLogResolver.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/ValidatedPathHeadersBodyLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/ValidatedPathHeadersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/ValidatedPathQueryHeadersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/ValidatedQueryHeadersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/Xs2aExecutionLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/PsuDataLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/SelectPsuAuthenticationMethodLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/StartScaprocessResponseLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/TransactionAuthorisationLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/UpdatePsuAuthenticationLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/Xs2aOauth2HeadersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/Xs2aOauth2ParametersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/Xs2aOauth2WithCodeParametersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/Xs2aResourceParametersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/Xs2aStandardHeadersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/Xs2aTransactionParametersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/Xs2aWithBalanceParametersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/common/Xs2aWithConsentIdHeadersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/consent/AccountAccessLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/consent/AccountReferenceLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/consent/ConsentInitiateHeadersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/consent/ConsentInitiateParametersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/consent/ConsentsLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/consent/Xs2aAuthorizedConsentParametersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/consent/Xs2aInitialConsentParametersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/context/AuthenticationObjectLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/context/BaseContextLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/context/ChallengeDataLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/context/ServiceContextLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/context/StartScaprocessResponseLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/context/TransactionListXs2aContextLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/context/Xs2aContextLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/context/Xs2aPisContextLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/payment/AddressLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/payment/AmountLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/payment/PaymentInitiateHeadersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/payment/PaymentInitiationJsonLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/payment/Xs2aAuthorizedPaymentParametersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/payment/Xs2aInitialPaymentParametersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/payment/Xs2aStartPaymentAuthorizationParametersLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/response/ResponseLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/response/TokenResponseLog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/domain/response/URILog.java create mode 100644 opba-protocols/xs2a-protocol/src/main/java/de/adorsys/opba/protocol/xs2a/util/logresolver/mapper/Xs2aDtoToLogObjectsMapper.java create mode 100644 opba-protocols/xs2a-protocol/src/test/java/de/adorsys/opba/protocol/xs2a/service/xs2a/dto/oauth2/Xs2aOauth2WithCodeParametersFromCtxTest.java create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/mapper-test-fixtures/account_list_context_from_ctx_list_account_request_with_extras_input.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/mapper-test-fixtures/account_list_context_from_ctx_list_account_request_with_extras_output.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/mapper-test-fixtures/transaction_list_xs2a_context_from_list_transactions_request_with_extras_input.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/mapper-test-fixtures/transaction_list_xs2a_context_from_list_transactions_request_with_extras_output.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/mapper-test-fixtures/xs2a_oauth2_with_code_parameters_from_xs2a_context_input.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/mapper-test-fixtures/xs2a_oauth2_with_code_parameters_from_xs2a_context_output.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/mapper-test-fixtures/xs2a_oauth2_with_code_parameters_from_xs2a_context_to_parameters_output.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/mapper-test-fixtures/xs2a_to_facade_response_mapper_list_transactions_request_input.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/mapper-test-fixtures/xs2a_to_facade_response_mapper_list_transactions_request_with_paging_input.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/mapper-test-fixtures/xs2a_to_facade_response_mapper_transactions_with_paging_input.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/mapper-test-fixtures/xs2a_to_facade_response_mapper_transactions_with_paging_output.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/xml-mapper-test-fixtures/camt-multibanking.xml create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/xml-mapper-test-fixtures/camt-sparkasse.xml create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/xml-mapper-test-fixtures/multibanking-output.json create mode 100644 opba-protocols/xs2a-protocol/src/test/resources/xml-mapper-test-fixtures/sparkasse-output.json create mode 100644 postman/collections/OPBA-AIS-HBCI-2-SCA-METHODS.postman_collection.json create mode 100644 postman/collections/OPBA-AIS-XS2A-EMBEDDED-2-SCA-METHODS.postman_collection.json create mode 100644 postman/collections/OPBA-PIS-SEPA-XS2A-EMBEDDED-2-SCA-METHODS-NO-LOGIN.postman_collection.json create mode 100644 postman/environments/OPBA-DEV-NO-SIG.postman_environment.json create mode 100644 postman/environments/OPBA-DEV.postman_environment.json create mode 100644 postman/environments/OPBA-LOCAL.postman_environment.json delete mode 100755 scripts/upload_coverage_to_codecov.sh diff --git a/.env b/.env new file mode 100644 index 0000000000..3a82a5fa08 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +WIREMOCK_STUBS_DIR=./opba-protocols/xs2a-protocol-tests/xs2a-bdd-wiremock/src/main/resources/mockedsandbox/restrecord/embedded/multi-sca/accounts/postbank \ No newline at end of file diff --git a/.github/workflows/daily-heavy-tests.yml b/.github/workflows/daily-heavy-tests.yml index 7606b64c8d..2ada29c615 100644 --- a/.github/workflows/daily-heavy-tests.yml +++ b/.github/workflows/daily-heavy-tests.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 with: @@ -22,8 +22,17 @@ jobs: with: access_token: ${{ secrets.GITHUB_TOKEN }} + - name: Free disk space + run: | + sudo apt clean + docker rmi $(docker image ls -aq) + docker system prune -af + echo "Available space" + df -h + - name: Build project and run default test suite - run: ./scripts/build_and_test.sh + run: | + ./scripts/build_and_test.sh env: MAVEN_OPTS: "-Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120" # Github-on-Azure tweaks - https://github.com/actions/virtual-environments/issues/1499 ENABLE_HEAVY_TESTS: true diff --git a/.github/workflows/deploy-to-integ.yml b/.github/workflows/deploy-to-integ.yml new file mode 100644 index 0000000000..b4a0a67cc6 --- /dev/null +++ b/.github/workflows/deploy-to-integ.yml @@ -0,0 +1,69 @@ +# Direct deploy to INTEG (no release-candidate step) +name: Build and deploy on integ + +on: + workflow_dispatch: + inputs: + gitRef: + description: 'Git reference (commit sha), optional, will use latest develop' + required: false + disableTests: + description: 'Disable tests' + required: true + default: 'false' + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Clone Repository (Latest) + uses: actions/checkout@v2 + if: github.event.inputs.gitRef == '' + + - name: Clone Repository (Custom commit SHA) + uses: actions/checkout@v2 + if: github.event.inputs.gitRef != '' + with: + ref: ${{ github.event.inputs.gitRef }} + + + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Free disk space + run: | + sudo apt clean + docker rmi $(docker image ls -aq) + docker system prune -af + echo "Available space" + df -h + + - name: Build project and run test suite + run: ./scripts/build_and_test.sh + env: + MAVEN_OPTS: "-Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120" # Github-on-Azure tweaks - https://github.com/actions/virtual-environments/issues/1499 + MOZ_HEADLESS: 1 + MVN_TESTS_DISABLED: ${{ github.event.inputs.disableTests }} + + - name: Deploy release-candidate artifacts to OpenShift + run: ./scripts/deploy_openshift_release_candidate.sh scripts/service.list + env: + OPENSHIFT_TOKEN: ${{ secrets.OPENSHIFT_TOKEN }} + RELEASE_CANDIDATE_DOMAIN: openshift-registry.adorsys.de + RELEASE_CANDIDATE_PROJECT_NAME: open-banking-gateway-integ + + - name: Build FireFly improter + run: ./scripts/build_firefly_exporter_mvn.sh + env: + MAVEN_OPTS: "-Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120" # Github-on-Azure tweaks - https://github.com/actions/virtual-environments/issues/1499 + MVN_TESTS_DISABLED: ${{ github.event.inputs.disableTests }} + + - name: Deploy FireFly Importer release-candidate artifacts to OpenShift + run: ./scripts/deploy_openshift_release_candidate.sh scripts/firefly-service.list + env: + OPENSHIFT_TOKEN: ${{ secrets.OPENSHIFT_TOKEN }} + RELEASE_CANDIDATE_DOMAIN: openshift-registry.adorsys.de + RELEASE_CANDIDATE_PROJECT_NAME: open-banking-gateway-integ diff --git a/.github/workflows/develop-push-merge.yml b/.github/workflows/develop-push-merge.yml index be76e73524..fcbb399d8b 100644 --- a/.github/workflows/develop-push-merge.yml +++ b/.github/workflows/develop-push-merge.yml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up JDK 11 @@ -21,6 +21,14 @@ jobs: with: access_token: ${{ secrets.GITHUB_TOKEN }} + - name: Free disk space + run: | + sudo apt clean + docker rmi $(docker image ls -aq) + docker system prune -af + echo "Available space" + df -h + - name: Build project and run default test suite run: ./scripts/build_and_test.sh env: @@ -53,8 +61,31 @@ jobs: env: OPENSHIFT_TOKEN: ${{ secrets.OPENSHIFT_TOKEN }} - - name: Upload coverage to CodeCov - run: ./scripts/upload_coverage_to_codecov.sh + # CODECOV Upload BEGIN + - uses: codecov/codecov-action@v1 + with: + directory: ./last-module-codecoverage/ + flags: backend + verbose: true + + - uses: codecov/codecov-action@v1 + with: + directory: ./consent-ui/ + flags: frontend + verbose: true + + - uses: codecov/codecov-action@v1 + with: + directory: ./fintech-examples/fintech-last-module-codecoverage/ + flags: fintech + verbose: true + + - uses: codecov/codecov-action@v1 + with: + directory: ./fintech-examples/fintech-ui/ + flags: fintech + verbose: true + # CODECOV Upload END - name: Update GitHub pages run: ./scripts/deploy_doc.sh diff --git a/.github/workflows/manual-heavy-tests.yml b/.github/workflows/manual-heavy-tests.yml new file mode 100644 index 0000000000..8bc8adc092 --- /dev/null +++ b/.github/workflows/manual-heavy-tests.yml @@ -0,0 +1,39 @@ +# Builds manually develop branch using `heavy tests` test suite +name: Develop branch heavy tests MANUAL build + +on: [workflow_dispatch] + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + with: + ref: develop + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.5.0 + with: + access_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Free disk space + run: | + sudo apt clean + docker rmi $(docker image ls -aq) + docker system prune -af + echo "Available space" + df -h + + - name: Build project and run default test suite + run: | + (while true; do df -h; sleep 60; done;) & + ./scripts/build_and_test.sh + env: + MAVEN_OPTS: "-Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120" # Github-on-Azure tweaks - https://github.com/actions/virtual-environments/issues/1499 + ENABLE_HEAVY_TESTS: true + MOZ_HEADLESS: 1 + diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 9ae842a65f..fd31b08743 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -9,7 +9,7 @@ on: jobs: build: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up JDK 11 @@ -22,7 +22,20 @@ jobs: with: access_token: ${{ secrets.GITHUB_TOKEN }} + - name: Free disk space + run: | + sudo apt clean + docker rmi $(docker image ls -aq) + docker system prune -af + echo "Available space" + df -h + - name: Build project and run default test suite run: ./scripts/build_and_test.sh env: MAVEN_OPTS: "-Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120" # Github-on-Azure tweaks - https://github.com/actions/virtual-environments/issues/1499 + + - name: Build FireFly improter + run: ./scripts/build_firefly_exporter_mvn.sh + env: + MAVEN_OPTS: "-Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120" # Github-on-Azure tweaks - https://github.com/actions/virtual-environments/issues/1499 diff --git a/.github/workflows/tag-release-candidate-push.yml b/.github/workflows/tag-release-candidate-push.yml index ee9006308a..5b0bede5b7 100644 --- a/.github/workflows/tag-release-candidate-push.yml +++ b/.github/workflows/tag-release-candidate-push.yml @@ -8,13 +8,22 @@ on: jobs: build: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up JDK 11 uses: actions/setup-java@v1 with: java-version: 11 + + - name: Free disk space + run: | + sudo apt clean + docker rmi $(docker image ls -aq) + docker system prune -af + echo "Available space" + df -h + - name: Build project and run test suite run: ./scripts/build_and_test.sh env: diff --git a/.github/workflows/tag-release-push.yml b/.github/workflows/tag-release-push.yml index 4d5d618b9f..d39dfe806a 100644 --- a/.github/workflows/tag-release-push.yml +++ b/.github/workflows/tag-release-push.yml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up JDK 11 @@ -16,6 +16,14 @@ jobs: with: java-version: 11 + - name: Free disk space + run: | + sudo apt clean + docker rmi $(docker image ls -aq) + docker system prune -af + echo "Available space" + df -h + - name: Promote artifacts from OpenShift to DockerHub run: ./scripts/promote_oc_image_to_dockerhub.sh scripts/service.list env: diff --git a/.gitignore b/.gitignore index 3e31e201c5..8c0ed31ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,9 @@ docs_for_site jgiven-reports logs site/ + +# Disable test profiles +opba-embedded-starter/src/main/resources/test-profiles/ + +# Disable developer secrets +**/application-my-secrets.yml diff --git a/README.md b/README.md index d2bc17657b..ae16edf860 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,24 @@ - Frontend: [![codecov-frontend](https://codecov.io/gh/adorsys/open-banking-gateway/branch/develop/graph/badge.svg?flag=frontend)](https://codecov.io/gh/adorsys/open-banking-gateway) - Example code: [![codecov-examples](https://codecov.io/gh/adorsys/open-banking-gateway/branch/develop/graph/badge.svg?flag=fintech)](https://codecov.io/gh/adorsys/open-banking-gateway) + +# Licensing model change to dual license: _AGPL v.3_ or _commercial license_ + +**Attention: this open-source project will change its licensing model as of _01.01.2022_!** + +Constantly evolving and extending scope, production traffic and support in open banking world call for high maintenance and service investments on our part. + +Henceforth, adorsys will offer all versions higher than v1.0 of Open Banking Gateway under a dual-license model. +Thus, this repository will be available either under Affero GNU General Public License v.3 (AGPL v.3) or alternatively under a commercial license agreement. + +We would like to thank all our users for their trust so far and are convinced that we will be able to provide an even better service going forward. + +For more information, advice for your implementation project or if your use case requires more time to adapt this change, +please contact us at [psd2@adorsys.com](mailto:psd2@adorsys.com). + +For additional details please see the section [FAQ on Licensing Change](#faq-on-licensing-change). + + # Open Banking Gateway Provides tools, adapters and connectors for transparent access to open banking apis. The initial effort focuses on the connectivity to banks that implement the European PSD2 directive either through one of the common market initiatives like : [The Berlin Group NextGenPSD2](https://www.berlin-group.org/psd2-access-to-bank-accounts), [The Open Banking UK](https://www.openbanking.org.uk/), [The Polish PSD2 API](https://polishapi.org/en/) or even through proprietary bank api like [the ING’s PSD2 API](https://developer.ing.com/openbanking/). @@ -35,7 +53,7 @@ In the Open Banking Context, a payment service user (PSU or banking account hold ## Big Picture The following picture displays the overall architecture of this banking gateway: -![High level architecture](docs/img/open-banking-gateway-arch-14-01-2020.png) +![High level architecture](docs/img/big-picture.png) ## Security concept @@ -75,6 +93,19 @@ The following picture displays the overall technical architecture concept of thi - [docker-compose-dev.yml](https://github.com/adorsys/open-banking-gateway/tree/develop/docker-compose-dev.yml) - docker-compose file in the project root for **Development** (requires building docker images) - [docker-compose.yml](https://github.com/adorsys/open-banking-gateway/tree/develop/docker-compose.yml) - docker-compose file in the project root for **Demo** (Images will be pulled from DockerHub) +## Postman scripts to play with API + +- [postman-collections](postman/collections) +- [postman-environments](postman/environments) (for playing use [this Postman environment](https://github.com/adorsys/open-banking-gateway/tree/develop/postman/environments/OPBA-DEV-NO-SIG.postman_environment.json)) + +### Postman collection details + +- [postman-ais-collection](https://github.com/adorsys/open-banking-gateway/tree/develop/postman/collections/OPBA-AIS-HBCI-OR-XS2A-EMBEDDED-2-SCA-METHODS.postman_collection.json) Xs2a-embedded or HBCI AIS (account information services) example - getting users' account and transactions list + +**Note:** Postman requires disabled request signing functionality - for that use Spring-profile `no-signature-filter`. +You can use our DEV environment (without signature check) if you import [this Postman environment](https://github.com/adorsys/open-banking-gateway/tree/develop/postman/environments/OPBA-DEV-NO-SIG.postman_environment.json) + + ## Information for developers: - Working with BPMN: As most protocols use BPMN, we have developed @@ -113,6 +144,7 @@ This project is designed to enable contribution from different sources, as the o * [Getting started](docs/getting_started.md) * [Contribution Guidelines](docs/ContributionGuidelines.md) + ## Authors & Contact * **[Francis Pouatcha](mailto:fpo@adorsys.de)** - *Initial work* - [adorsys](https://www.adorsys.de) @@ -121,7 +153,93 @@ See also the list of [contributors](https://github.com/adorsys/open-banking-gate For commercial support please contact **[adorsys Team](https://adorsys.de/)**. + ## License -This project is licensed under the Apache License version 2.0 - see the [LICENSE](LICENSE) file for details +This project is licensed **(until 01.01.2022)** under the Apache License version 2.0 - see the [LICENSE](LICENSE) file for details + + +## FAQ on Licensing Change + +### What is a dual-licensing model? + +Under a dual-licensing model, our product is available under two licenses: + +- [The Affero GNU General Public License v3 (AGPL v3)](https://www.gnu.org/licenses/agpl-3.0.en.html) +- A proprietary commercial license + +If you are a developer or business that would like to review our products in detail, test and implement in your +open-source projects and share the changes back to the community, the product repository is freely available under AGPL v3. + +If you are a business that would like to implement our products in a commercial setting and would like to protect your +individual changes, we offer the option to license our products under a commercial license. + +This change will still allow free access and ensure openness under AGPL v3 but with assurance of committing any +alterations or extensions back to the project and preventing redistribution of such implementations under commercial license. + + + +### Will there be any differences between the open-source and commercially licensed versions of your products? + +Our public release frequency will be reduced as our focus shifts towards the continuous maintenance of the commercial +version. Nevertheless, we are committed to also provide open-source releases of our products on a regular basis as per our release policy. + +For customers with a commercial license, we will offer new intermediate releases in a more frequent pace. + + + +### Does this mean that this product is no longer open source? + +No, the product will still be published and available on GitHub under an OSI-approved open-source license (AGPL v3). + + + +### What about adorsys’ commitment to open source? Will adorsys provide future product releases on GitHub? + +We at adorsys are committed to continue actively participating in the open-source community. Our products remain +licensed under OSI-approved open-source licenses, and we are looking forward to expanding our product portfolio on GitHub even further. + + + +### How does the change impact me if I already use the open-source edition of your product? + +All currently published versions until v1.0 will remain under their current Apache 2.0 license and its respective +requirements and you may continue using it as-is. To upgrade to future versions, you will be required to either abide +by the requirements of AGPL v3, including documenting and sharing your implemented changes to the product when +distributing, or alternatively approach us to obtain a commercial license. + + + +### What if I cannot adjust to the new licensing model until 01.01.2022? Can I extend the deadline? + +We understand that adjustment to licensing changes can take time and therefore are open to discuss extension options on +an individual basis. For inquiries please contact us at [psd2@adorsys.com](mailto:psd2@adorsys.com). + + + +### Which versions of the product are affected? + +All versions of Open Banking Gateway after v1.0 will be affected by the licensing changes and move to a dual-licensing model. + + + +### What will happen to older, Apache 2.0 licensed product versions? + +All older Apache 2.0 licensed versions prior and including v1.0 will remain available under their existing license. + + + +### What open-source products from Adorsys are affected by the licensing change? + +The following products are affected: + - [XS2A Core](https://github.com/adorsys/xs2a) + - [XS2A Sandbox & ModelBank](https://github.com/adorsys/XS2A-Sandbox) + - [Open Banking Gateway](https://github.com/adorsys/open-banking-gateway) incl. [XS2A Adapters](https://github.com/adorsys/xs2a-adapter) + - [SmartAnalytics](https://github.com/adorsys/smartanalytics) + - [Datasafe](https://github.com/adorsys/datasafe) + + +### I’m using one of these products indirectly via some software integrator. How does the licensing change affect me? +The licensing change does not affect you as user, but it is relevant to your provider who has used our product in their +solution implementation. In case of uncertainty please contact your service provider or approach us at [psd2@adorsys.com](mailto:psd2@adorsys.com). diff --git a/consent-ui/src/app/ais/ais-routing.module.ts b/consent-ui/src/app/ais/ais-routing.module.ts index 2726fd4701..a35dd0ef30 100644 --- a/consent-ui/src/app/ais/ais-routing.module.ts +++ b/consent-ui/src/app/ais/ais-routing.module.ts @@ -14,6 +14,7 @@ import { EntryPageTransactionsComponent } from './entry-page/initiation/transact import { TransactionsConsentReviewComponent } from './entry-page/initiation/transactions/transactions-consent-review/transactions-consent-review.component'; import { ToAspspRedirectionComponent } from './to-aspsp-page/to-aspsp-redirection.component'; import { ConsentSharingComponent } from './entry-page/initiation/consent-sharing/consent-sharing.component'; +import { WaitForDecoupled } from "./wait-for-decoupled/wait-for-decoupled"; const routes: Routes = [ { @@ -39,6 +40,7 @@ const routes: Routes = [ ] }, { path: ToAspspRedirectionComponent.ROUTE, component: ToAspspRedirectionComponent }, + { path: WaitForDecoupled.ROUTE, component: WaitForDecoupled }, { path: ResultPageComponent.ROUTE, component: ResultPageComponent }, { path: 'authenticate', diff --git a/consent-ui/src/app/ais/ais.module.ts b/consent-ui/src/app/ais/ais.module.ts index 7f776f5e96..04ae59d488 100644 --- a/consent-ui/src/app/ais/ais.module.ts +++ b/consent-ui/src/app/ais/ais.module.ts @@ -7,7 +7,6 @@ import { EnterPinPageComponent } from './enter-pin-page/enter-pin-page.component import { EntryPageComponent } from './entry-page/entry-page.component'; import { ScaSelectPageComponent } from './sca-select-page/sca-select-page.component'; import { EnterTanPageComponent } from './enter-tan-page/enter-tan-page.component'; -import { AccountDetailsComponent } from './common/account-details/account-details.component'; import { ErrorPageComponent } from './error-page/error-page.component'; import { RouteBasedCardWithSidebarComponent } from './route-based-card-with-sidebar/route-based-card-with-sidebar.component'; import { SidebarComponent } from './sidebar/sidebar.component'; @@ -24,6 +23,7 @@ import { ToAspspRedirectionComponent } from './to-aspsp-page/to-aspsp-redirectio import { ConsentInfoComponent } from './components/consent-info/consent-info.component'; import { ConsentSharingComponent } from './entry-page/initiation/consent-sharing/consent-sharing.component'; import { SharedModule } from '../common/shared.module'; +import { WaitForDecoupled } from "./wait-for-decoupled/wait-for-decoupled"; @NgModule({ declarations: [ @@ -32,7 +32,6 @@ import { SharedModule } from '../common/shared.module'; EntryPageComponent, ScaSelectPageComponent, EnterTanPageComponent, - AccountDetailsComponent, ConsentAccountAccessSelectionComponent, EntryPageTransactionsComponent, ErrorPageComponent, @@ -47,7 +46,8 @@ import { SharedModule } from '../common/shared.module'; EntryPageAccountsComponent, ToAspspRedirectionComponent, ConsentInfoComponent, - ConsentSharingComponent + ConsentSharingComponent, + WaitForDecoupled ], imports: [SharedModule, AisRoutingModule, AngularIbanModule] }) diff --git a/consent-ui/src/app/ais/common/account-details/account-details.component.html b/consent-ui/src/app/ais/common/account-details/account-details.component.html deleted file mode 100644 index 8eeff2890d..0000000000 --- a/consent-ui/src/app/ais/common/account-details/account-details.component.html +++ /dev/null @@ -1 +0,0 @@ -

account-details works!

diff --git a/consent-ui/src/app/ais/common/account-details/account-details.component.spec.ts b/consent-ui/src/app/ais/common/account-details/account-details.component.spec.ts deleted file mode 100644 index ab75813700..0000000000 --- a/consent-ui/src/app/ais/common/account-details/account-details.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { AccountDetailsComponent } from './account-details.component'; - -describe('AccountDetailsComponent', () => { - let component: AccountDetailsComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ AccountDetailsComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(AccountDetailsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/consent-ui/src/app/ais/common/account-details/account-details.component.ts b/consent-ui/src/app/ais/common/account-details/account-details.component.ts deleted file mode 100644 index be1e250aa6..0000000000 --- a/consent-ui/src/app/ais/common/account-details/account-details.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -@Component({ - selector: 'consent-app-account-details', - templateUrl: './account-details.component.html', - styleUrls: ['./account-details.component.scss'] -}) -export class AccountDetailsComponent implements OnInit { - - constructor() { } - - ngOnInit() { - } - -} diff --git a/consent-ui/src/app/ais/common/consent-util.ts b/consent-ui/src/app/ais/common/consent-util.ts index c2a072c6ea..4a08ed228d 100644 --- a/consent-ui/src/app/ais/common/consent-util.ts +++ b/consent-ui/src/app/ais/common/consent-util.ts @@ -10,6 +10,20 @@ export class ConsentUtil { return storageService.getConsentObject(authorizationId, () => new AisConsentToGrant()); } + public static rollbackConsent(authorizationId: string, sessionService: SessionService) { + const consentObj = ConsentUtil.getOrDefault(authorizationId, sessionService); + consentObj.consent.access.availableAccounts = null; + consentObj.consent.access.allPsd2 = null; + consentObj.consent.access.accounts = null; + consentObj.consent.access.balances = null; + consentObj.consent.access.transactions = null; + sessionService.setConsentObject(authorizationId, consentObj); + } + + public static isEmptyObject(obj: Object) { + return null === obj || !obj || (Object.keys(obj).length === 0); + } + private static initializeConsentObject(): AisConsentToGrant { const aisConsent = new AisConsentToGrant(); // FIXME: These fields MUST be initialized by FinTech through API and user can only adjust it. diff --git a/consent-ui/src/app/ais/common/constant/constant.ts b/consent-ui/src/app/ais/common/constant/constant.ts new file mode 100644 index 0000000000..217214127f --- /dev/null +++ b/consent-ui/src/app/ais/common/constant/constant.ts @@ -0,0 +1,3 @@ +export const DATA_PATTERN = '\\d{4}-\\d{2}-\\d{2}'; +// The consent can be used max times per day +export const MAX_FREQUENCY_PER_DAY = 999; diff --git a/consent-ui/src/app/ais/common/date-util.ts b/consent-ui/src/app/ais/common/date-util.ts new file mode 100644 index 0000000000..a98d793262 --- /dev/null +++ b/consent-ui/src/app/ais/common/date-util.ts @@ -0,0 +1,25 @@ +import {AbstractControl, ValidatorFn} from '@angular/forms'; + +export class DateUtil { + public static getActualDate(): string { + const result = new Date(); + result.setDate(result.getDate()); + return result.toISOString().split('T')[0]; + } + + public static isDateNotInThePastValidator(): ValidatorFn { + return (control: AbstractControl): { [key: string]: boolean } | null => { + + if(!(control && control.value)) { + return null; + } + + const actualDate = new Date(this.getActualDate()); + const date = new Date(control.value); + + return date < actualDate + ? { invalidDate: true } + : null; + } + } +} diff --git a/consent-ui/src/app/ais/common/dto/ais-consent.ts b/consent-ui/src/app/ais/common/dto/ais-consent.ts index c5acbeb1fd..c987b45478 100644 --- a/consent-ui/src/app/ais/common/dto/ais-consent.ts +++ b/consent-ui/src/app/ais/common/dto/ais-consent.ts @@ -1,3 +1,6 @@ +import {ConsentAuth} from '../../../api'; +import SupportedType = ConsentAuth.SupportedConsentTypesEnum; + export class AisConsentToGrant { level: AccountAccessLevel; consent: AisConsent; @@ -40,3 +43,11 @@ export enum AccountAccessLevel { ALL_ACCOUNTS_WITH_BALANCES = 'ALL_ACCOUNTS_WITH_BALANCES', FINE_GRAINED = 'FINE_GRAINED' } + + +export const AccountAccessLevelAspspConsentSupport = new Map>([ + [AccountAccessLevel.ALL_ACCOUNTS, new Set([SupportedType.GLOBALACCOUNTS, SupportedType.GLOBALALL])], + [AccountAccessLevel.ALL_PSD2, new Set([SupportedType.GLOBALALL])], + [AccountAccessLevel.ALL_ACCOUNTS_WITH_BALANCES, new Set([SupportedType.GLOBALALL])], + [AccountAccessLevel.FINE_GRAINED, new Set([SupportedType.DEDICATEDALL, SupportedType.GLOBALALL, SupportedType.GLOBALACCOUNTS])] +]); diff --git a/consent-ui/src/app/ais/components/consent-info/consent-info.component.ts b/consent-ui/src/app/ais/components/consent-info/consent-info.component.ts index f5827c1a49..714a410b3c 100644 --- a/consent-ui/src/app/ais/components/consent-info/consent-info.component.ts +++ b/consent-ui/src/app/ais/components/consent-info/consent-info.component.ts @@ -1,6 +1,5 @@ import { Component, OnInit } from '@angular/core'; import { AccountAccessLevel, AisConsentToGrant } from '../../common/dto/ais-consent'; -import { StubUtil } from '../../../common/utils/stub-util'; import { ActivatedRoute, Router } from '@angular/router'; import { SessionService } from '../../../common/session.service'; import { ConsentUtil } from '../../common/consent-util'; diff --git a/consent-ui/src/app/ais/entry-page/initiation/accounts/accounts-consent-review/accounts-consent-review.component.html b/consent-ui/src/app/ais/entry-page/initiation/accounts/accounts-consent-review/accounts-consent-review.component.html index c5189aa56a..2d1f7dc85c 100644 --- a/consent-ui/src/app/ais/entry-page/initiation/accounts/accounts-consent-review/accounts-consent-review.component.html +++ b/consent-ui/src/app/ais/entry-page/initiation/accounts/accounts-consent-review/accounts-consent-review.component.html @@ -6,6 +6,7 @@

Access permission for accounts in {{ aspspName }}

Review an account access level for {{ finTechName }}

+
@@ -33,23 +34,65 @@

Access permission for accounts in {{ aspspName }}

-
+
approval

- The consent will be valid until {{ aisConsent.consent.validUntil }} + The consent will be valid until:

+
-
+
24_hours

- The consent can be used {{ aisConsent.consent.frequencyPerDay }} times per day + The consent can be used:

+ + times per day +
+
+ + +
+
+ + The value must be greater than or equal to 1. + + + Incorrect date format or date (past dates cannot be used). +
+
- +
diff --git a/consent-ui/src/app/ais/entry-page/initiation/accounts/accounts-consent-review/accounts-consent-review.component.ts b/consent-ui/src/app/ais/entry-page/initiation/accounts/accounts-consent-review/accounts-consent-review.component.ts index 8a9d164410..bd02c168b5 100644 --- a/consent-ui/src/app/ais/entry-page/initiation/accounts/accounts-consent-review/accounts-consent-review.component.ts +++ b/consent-ui/src/app/ais/entry-page/initiation/accounts/accounts-consent-review/accounts-consent-review.component.ts @@ -4,11 +4,13 @@ import { SharedRoutes } from '../../common/shared-routes'; import { AccountAccessLevel, AisConsentToGrant } from '../../../../common/dto/ais-consent'; import { StubUtil } from '../../../../../common/utils/stub-util'; import { ActivatedRoute, Router } from '@angular/router'; -import { FormBuilder } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { SessionService } from '../../../../../common/session.service'; import { ConsentUtil } from '../../../../common/consent-util'; import { ApiHeaders } from '../../../../../api/api.headers'; import { ConsentAuth, UpdateConsentAuthorizationService, PsuAuthRequest } from '../../../../../api'; +import { DATA_PATTERN, MAX_FREQUENCY_PER_DAY } from '../../../../common/constant/constant'; +import { DateUtil } from '../../../../common/date-util'; @Component({ selector: 'consent-app-accounts-consent-review', @@ -16,6 +18,9 @@ import { ConsentAuth, UpdateConsentAuthorizationService, PsuAuthRequest } from ' styleUrls: ['./accounts-consent-review.component.scss'] }) export class AccountsConsentReviewComponent implements OnInit { + + consentReviewForm: FormGroup; + constructor( private location: Location, private router: Router, @@ -29,6 +34,7 @@ export class AccountsConsentReviewComponent implements OnInit { accountAccessLevel = AccountAccessLevel; + public actualDate: string; public finTechName: string; public aspspName: string; public aisConsent: AisConsentToGrant; @@ -41,10 +47,22 @@ export class AccountsConsentReviewComponent implements OnInit { this.aisConsent = ConsentUtil.getOrDefault(this.authorizationId, this.sessionService); this.aspspName = this.sessionService.getBankName(res.authId); this.finTechName = this.sessionService.getFintechName(res.authId); + this.actualDate = DateUtil.getActualDate(); }); + this.createForm(); } onConfirm() { + if (this.consentReviewForm.invalid) { + return; + } + + this.aisConsent.consent.recurringIndicator = this.consentReviewForm.value.recurringIndicator; + this.aisConsent.consent.validUntil = this.consentReviewForm.value.validUntilDate; + this.aisConsent.consent.frequencyPerDay = this.consentReviewForm.value.frequencyPerDay; + + this.sessionService.setConsentObject(this.authorizationId, this.aisConsent); + const body = { extras: this.aisConsent.extras } as PsuAuthRequest; if (this.aisConsent) { @@ -54,14 +72,13 @@ export class AccountsConsentReviewComponent implements OnInit { this.updateConsentAuthorizationService .embeddedUsingPOST( this.authorizationId, - StubUtil.X_XSRF_TOKEN, StubUtil.X_REQUEST_ID, this.sessionService.getRedirectCode(this.authorizationId), body, 'response' ) .subscribe((res) => { - this.sessionService.setRedirectCode(this.authorizationId, res.headers.get(ApiHeaders.REDIRECT_CODE)); + this.sessionService.setRedirectCode(this.authorizationId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); window.location.href = res.headers.get(ApiHeaders.LOCATION); }); } @@ -69,4 +86,34 @@ export class AccountsConsentReviewComponent implements OnInit { onBack() { this.location.back(); } + + private createForm() { + this.consentReviewForm = this.formBuilder.group({ + recurringIndicator: this.aisConsent.consent.recurringIndicator, + validUntilDate: [ + this.aisConsent.consent.validUntil, + [ + Validators.required, + Validators.pattern(DATA_PATTERN), + DateUtil.isDateNotInThePastValidator() + ] + ], + frequencyPerDay: [ + this.aisConsent.consent.frequencyPerDay, + [ + Validators.required, + Validators.min(1), + Validators.max(MAX_FREQUENCY_PER_DAY) + ] + ] + }) + } + + get validUntilDate() { + return this.consentReviewForm.get('validUntilDate') + } + + get frequencyPerDay() { + return this.consentReviewForm.get('frequencyPerDay') + } } diff --git a/consent-ui/src/app/ais/entry-page/initiation/accounts/entry-page-accounts/entry-page-accounts.component.ts b/consent-ui/src/app/ais/entry-page/initiation/accounts/entry-page-accounts/entry-page-accounts.component.ts index 4053ef675d..db0dc1d448 100644 --- a/consent-ui/src/app/ais/entry-page/initiation/accounts/entry-page-accounts/entry-page-accounts.component.ts +++ b/consent-ui/src/app/ais/entry-page/initiation/accounts/entry-page-accounts/entry-page-accounts.component.ts @@ -1,8 +1,8 @@ -import { Component, OnInit } from '@angular/core'; -import { Access } from '../../common/initial-consent/consent-account-access-selection.component'; -import { AccountsConsentReviewComponent } from '../accounts-consent-review/accounts-consent-review.component'; -import { DedicatedAccessComponent } from '../../common/dedicated-access/dedicated-access.component'; -import { AccountAccessLevel } from '../../../../common/dto/ais-consent'; +import {Component, OnInit} from '@angular/core'; +import {Access} from '../../common/initial-consent/consent-account-access-selection.component'; +import {AccountsConsentReviewComponent} from '../accounts-consent-review/accounts-consent-review.component'; +import {DedicatedAccessComponent} from '../../common/dedicated-access/dedicated-access.component'; +import {AccountAccessLevel} from '../../../../common/dto/ais-consent'; @Component({ selector: 'consent-app-entry-page-accounts', diff --git a/consent-ui/src/app/ais/entry-page/initiation/common/dedicated-access/dedicated-access.component.spec.ts b/consent-ui/src/app/ais/entry-page/initiation/common/dedicated-access/dedicated-access.component.spec.ts index 1c7eb39076..7f8615d4db 100644 --- a/consent-ui/src/app/ais/entry-page/initiation/common/dedicated-access/dedicated-access.component.spec.ts +++ b/consent-ui/src/app/ais/entry-page/initiation/common/dedicated-access/dedicated-access.component.spec.ts @@ -64,7 +64,7 @@ describe('DedicatedAccessComponent', () => { }, frequencyPerDay: 24, recurringIndicator: true, - validUntil: '2021-06-24' + validUntil: '2099-06-24' } }; consentUtilSpy = spyOn(ConsentUtil, 'getOrDefault').and.returnValue(mockData); diff --git a/consent-ui/src/app/ais/entry-page/initiation/common/dedicated-access/dedicated-access.component.ts b/consent-ui/src/app/ais/entry-page/initiation/common/dedicated-access/dedicated-access.component.ts index c1c64657bc..f073aeb8d0 100644 --- a/consent-ui/src/app/ais/entry-page/initiation/common/dedicated-access/dedicated-access.component.ts +++ b/consent-ui/src/app/ais/entry-page/initiation/common/dedicated-access/dedicated-access.component.ts @@ -61,14 +61,7 @@ export class DedicatedAccessComponent implements OnInit { } onBack() { - const consentObj = ConsentUtil.getOrDefault(this.authorizationId, this.sessionService); - consentObj.consent.access.availableAccounts = null; - consentObj.consent.access.allPsd2 = null; - consentObj.consent.access.accounts = null; - consentObj.consent.access.balances = null; - consentObj.consent.access.transactions = null; - this.sessionService.setConsentObject(this.authorizationId, consentObj); - + ConsentUtil.rollbackConsent(this.authorizationId, this.sessionService); this.location.back(); } diff --git a/consent-ui/src/app/ais/entry-page/initiation/common/initial-consent/consent-account-access-selection.component.html b/consent-ui/src/app/ais/entry-page/initiation/common/initial-consent/consent-account-access-selection.component.html index 7ace1e54ba..d7e1df22c0 100644 --- a/consent-ui/src/app/ais/entry-page/initiation/common/initial-consent/consent-account-access-selection.component.html +++ b/consent-ui/src/app/ais/entry-page/initiation/common/initial-consent/consent-account-access-selection.component.html @@ -19,7 +19,7 @@

Access permission for accounts in {{ aspspName }}

Please choose an account access level for {{ finTechName }}

-
+
-
+
approval

- The consent will be valid until {{ aisConsent.consent.validUntil }} + The consent will be valid until:

+
-
+
24_hours

- The consent can be used {{ aisConsent.consent.frequencyPerDay }} times per day + The consent can be used:

+ + times per day +
+
+ + +
+
+ + The value must be greater than or equal to 1. + + + Incorrect date format or date (past dates cannot be used). +
+
- +
diff --git a/consent-ui/src/app/ais/entry-page/initiation/transactions/transactions-consent-review/transactions-consent-review.component.spec.ts b/consent-ui/src/app/ais/entry-page/initiation/transactions/transactions-consent-review/transactions-consent-review.component.spec.ts index 15f48bbe93..ea22e5c79c 100644 --- a/consent-ui/src/app/ais/entry-page/initiation/transactions/transactions-consent-review/transactions-consent-review.component.spec.ts +++ b/consent-ui/src/app/ais/entry-page/initiation/transactions/transactions-consent-review/transactions-consent-review.component.spec.ts @@ -42,6 +42,7 @@ describe('TransactionsConsentReviewComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(TransactionsConsentReviewComponent); component = fixture.componentInstance; + component.ngOnInit(); fixture.detectChanges(); consentAuthorizationService = TestBed.inject(UpdateConsentAuthorizationService); }); @@ -56,6 +57,7 @@ describe('TransactionsConsentReviewComponent', () => { expect(location.back).toHaveBeenCalled(); }); + // FIXME Disabled as DateUtil.isDateNotInThePastValidator seem to cause 'undefined' error in control validation it('should confirm transaction when confirm button is pressed', () => { consentAuthorizationServiceSpy = spyOn(consentAuthorizationService, 'embeddedUsingPOST').and.returnValue(of()); component.onConfirm(); diff --git a/consent-ui/src/app/ais/entry-page/initiation/transactions/transactions-consent-review/transactions-consent-review.component.ts b/consent-ui/src/app/ais/entry-page/initiation/transactions/transactions-consent-review/transactions-consent-review.component.ts index 05cac4b2ab..3285d5d9c2 100644 --- a/consent-ui/src/app/ais/entry-page/initiation/transactions/transactions-consent-review/transactions-consent-review.component.ts +++ b/consent-ui/src/app/ais/entry-page/initiation/transactions/transactions-consent-review/transactions-consent-review.component.ts @@ -1,14 +1,16 @@ -import { Component, OnInit } from '@angular/core'; -import { Location } from '@angular/common'; -import { SharedRoutes } from '../../common/shared-routes'; -import { ActivatedRoute, Router } from '@angular/router'; -import { FormBuilder } from '@angular/forms'; -import { SessionService } from '../../../../../common/session.service'; -import { AccountAccessLevel, AisConsentToGrant } from '../../../../common/dto/ais-consent'; -import { StubUtil } from '../../../../../common/utils/stub-util'; -import { ConsentUtil } from '../../../../common/consent-util'; -import { ApiHeaders } from '../../../../../api/api.headers'; -import { ConsentAuth, UpdateConsentAuthorizationService, PsuAuthRequest } from '../../../../../api'; +import {Component, OnInit} from '@angular/core'; +import {Location} from '@angular/common'; +import {SharedRoutes} from '../../common/shared-routes'; +import {ActivatedRoute, Router} from '@angular/router'; +import {FormBuilder, FormGroup, Validators} from '@angular/forms'; +import {SessionService} from '../../../../../common/session.service'; +import {AccountAccessLevel, AisConsentToGrant} from '../../../../common/dto/ais-consent'; +import {StubUtil} from '../../../../../common/utils/stub-util'; +import {ConsentUtil} from '../../../../common/consent-util'; +import {ApiHeaders} from '../../../../../api/api.headers'; +import {ConsentAuth, UpdateConsentAuthorizationService, PsuAuthRequest} from '../../../../../api'; +import {DATA_PATTERN, MAX_FREQUENCY_PER_DAY} from '../../../../common/constant/constant'; +import {DateUtil} from '../../../../common/date-util'; @Component({ selector: 'consent-app-transactions-consent-review', @@ -16,6 +18,9 @@ import { ConsentAuth, UpdateConsentAuthorizationService, PsuAuthRequest } from ' styleUrls: ['./transactions-consent-review.component.scss'] }) export class TransactionsConsentReviewComponent implements OnInit { + + consentReviewForm: FormGroup; + constructor( private location: Location, private router: Router, @@ -28,9 +33,9 @@ export class TransactionsConsentReviewComponent implements OnInit { public static ROUTE = SharedRoutes.REVIEW; accountAccessLevel = AccountAccessLevel; + public actualDate: string; public finTechName: string; public aspspName: string; - public aisConsent: AisConsentToGrant; private authorizationId: string; @@ -41,10 +46,22 @@ export class TransactionsConsentReviewComponent implements OnInit { this.aspspName = this.sessionService.getBankName(res.authId); this.finTechName = this.sessionService.getFintechName(res.authId); this.aisConsent = ConsentUtil.getOrDefault(this.authorizationId, this.sessionService); + this.actualDate = DateUtil.getActualDate(); }); + this.createForm(); } onConfirm() { + if (this.consentReviewForm.invalid) { + return; + } + + this.aisConsent.consent.recurringIndicator = this.consentReviewForm.value.recurringIndicator; + this.aisConsent.consent.validUntil = this.consentReviewForm.value.validUntilDate; + this.aisConsent.consent.frequencyPerDay = this.consentReviewForm.value.frequencyPerDay; + + this.sessionService.setConsentObject(this.authorizationId, this.aisConsent); + const body = { extras: this.aisConsent.extras } as PsuAuthRequest; if (this.aisConsent) { @@ -54,14 +71,13 @@ export class TransactionsConsentReviewComponent implements OnInit { this.updateConsentAuthorizationService .embeddedUsingPOST( this.authorizationId, - StubUtil.X_XSRF_TOKEN, StubUtil.X_REQUEST_ID, this.sessionService.getRedirectCode(this.authorizationId), body, 'response' ) .subscribe((res) => { - this.sessionService.setRedirectCode(this.authorizationId, res.headers.get(ApiHeaders.REDIRECT_CODE)); + this.sessionService.setRedirectCode(this.authorizationId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); window.location.href = res.headers.get(ApiHeaders.LOCATION); }); } @@ -69,4 +85,34 @@ export class TransactionsConsentReviewComponent implements OnInit { onBack() { this.location.back(); } + + private createForm() { + this.consentReviewForm = this.formBuilder.group({ + recurringIndicator: this.aisConsent.consent.recurringIndicator, + validUntilDate: [ + this.aisConsent.consent.validUntil, + [ + Validators.required, + Validators.pattern(DATA_PATTERN), + DateUtil.isDateNotInThePastValidator() + ] + ], + frequencyPerDay: [ + this.aisConsent.consent.frequencyPerDay, + [ + Validators.required, + Validators.min(1), + Validators.max(MAX_FREQUENCY_PER_DAY) + ] + ] + }) + } + + get validUntilDate() { + return this.consentReviewForm.get('validUntilDate') + } + + get frequencyPerDay() { + return this.consentReviewForm.get('frequencyPerDay') + } } diff --git a/consent-ui/src/app/ais/result-page/result-page.component.ts b/consent-ui/src/app/ais/result-page/result-page.component.ts index 5d1ca6f5f6..08ceede21d 100644 --- a/consent-ui/src/app/ais/result-page/result-page.component.ts +++ b/consent-ui/src/app/ais/result-page/result-page.component.ts @@ -1,14 +1,13 @@ -import { Component, OnInit } from '@angular/core'; -import { Location } from '@angular/common'; -import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; +import {Component, OnInit} from '@angular/core'; +import {Location} from '@angular/common'; +import {ActivatedRoute, ActivatedRouteSnapshot} from '@angular/router'; -import { StubUtil } from '../../common/utils/stub-util'; -import { AisConsentToGrant } from '../common/dto/ais-consent'; -import { SessionService } from '../../common/session.service'; -import { ConsentUtil } from '../common/consent-util'; -import { ApiHeaders } from '../../api/api.headers'; -import { UpdateConsentAuthorizationService } from '../../api'; -import { AuthStateConsentAuthorizationService, DenyRequest } from '../../api'; +import {StubUtil} from '../../common/utils/stub-util'; +import {AisConsentToGrant} from '../common/dto/ais-consent'; +import {SessionService} from '../../common/session.service'; +import {ConsentUtil} from '../common/consent-util'; +import {ApiHeaders} from '../../api/api.headers'; +import {AuthStateConsentAuthorizationService, UpdateConsentAuthorizationService} from '../../api'; @Component({ selector: 'consent-app-result-page', @@ -53,8 +52,6 @@ export class ResultPageComponent implements OnInit { .denyUsingPOST( this.authorizationId, StubUtil.X_REQUEST_ID, // TODO: real values instead of stubs - StubUtil.X_XSRF_TOKEN, // TODO: real values instead of stubs - {} as DenyRequest, 'response' ) .subscribe((res) => { @@ -65,7 +62,7 @@ export class ResultPageComponent implements OnInit { private loadRedirectUri(authId: string, redirectCode: string) { this.authStateConsentAuthorizationService.authUsingGET(authId, redirectCode, 'response').subscribe((res) => { console.log(res); - this.sessionService.setRedirectCode(authId, res.headers.get(ApiHeaders.REDIRECT_CODE)); + this.sessionService.setRedirectCode(authId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); this.redirectTo = res.headers.get(ApiHeaders.LOCATION); }); } diff --git a/consent-ui/src/app/ais/to-aspsp-page/to-aspsp-redirection.component.ts b/consent-ui/src/app/ais/to-aspsp-page/to-aspsp-redirection.component.ts index 631213e61d..c78e061771 100644 --- a/consent-ui/src/app/ais/to-aspsp-page/to-aspsp-redirection.component.ts +++ b/consent-ui/src/app/ais/to-aspsp-page/to-aspsp-redirection.component.ts @@ -1,15 +1,15 @@ -import { Component, OnInit } from '@angular/core'; -import { Location } from '@angular/common'; -import { ActivatedRoute } from '@angular/router'; +import {Component, OnInit} from '@angular/core'; +import {Location} from '@angular/common'; +import {ActivatedRoute} from '@angular/router'; -import { AisConsentToGrant } from '../common/dto/ais-consent'; -import { StubUtil } from '../../common/utils/stub-util'; -import { SessionService } from '../../common/session.service'; -import { ConsentUtil } from '../common/consent-util'; -import { ApiHeaders } from '../../api/api.headers'; -import { Action } from '../../common/utils/action'; -import { AuthStateConsentAuthorizationService, DenyRequest, UpdateConsentAuthorizationService } from '../../api'; -import { combineLatest } from 'rxjs'; +import {AisConsentToGrant} from '../common/dto/ais-consent'; +import {StubUtil} from '../../common/utils/stub-util'; +import {SessionService} from '../../common/session.service'; +import {ConsentUtil} from '../common/consent-util'; +import {ApiHeaders} from '../../api/api.headers'; +import {Action} from '../../common/utils/action'; +import {AuthStateConsentAuthorizationService, UpdateConsentAuthorizationService} from '../../api'; +import {combineLatest} from 'rxjs'; @Component({ selector: 'consent-app-to-aspsp-redirection', @@ -57,7 +57,7 @@ export class ToAspspRedirectionComponent implements OnInit { this.authStateConsentAuthorizationService .authUsingGET(this.authorizationId, this.sessionService.getRedirectCode(this.authorizationId), 'response') .subscribe((res) => { - this.sessionService.setRedirectCode(this.authorizationId, res.headers.get(ApiHeaders.REDIRECT_CODE)); + this.sessionService.setRedirectCode(this.authorizationId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); this.redirectTo = res.headers.get(ApiHeaders.LOCATION); }); } @@ -67,8 +67,6 @@ export class ToAspspRedirectionComponent implements OnInit { .denyUsingPOST( this.authorizationId, StubUtil.X_REQUEST_ID, // TODO: real values instead of stubs - StubUtil.X_XSRF_TOKEN, // TODO: real values instead of stubs - {} as DenyRequest, 'response' ) .subscribe((res) => { diff --git a/consent-ui/src/app/ais/wait-for-decoupled/wait-for-decoupled.html b/consent-ui/src/app/ais/wait-for-decoupled/wait-for-decoupled.html new file mode 100644 index 0000000000..29201e5112 --- /dev/null +++ b/consent-ui/src/app/ais/wait-for-decoupled/wait-for-decoupled.html @@ -0,0 +1,10 @@ +
+
+
+

Confirm consent on your application

+

+ {{authResponse?.scaMethodSelected.explanation}} +

+
+
+
diff --git a/consent-ui/src/app/ais/common/account-details/account-details.component.scss b/consent-ui/src/app/ais/wait-for-decoupled/wait-for-decoupled.scss similarity index 100% rename from consent-ui/src/app/ais/common/account-details/account-details.component.scss rename to consent-ui/src/app/ais/wait-for-decoupled/wait-for-decoupled.scss diff --git a/consent-ui/src/app/ais/wait-for-decoupled/wait-for-decoupled.ts b/consent-ui/src/app/ais/wait-for-decoupled/wait-for-decoupled.ts new file mode 100644 index 0000000000..9c31723ee6 --- /dev/null +++ b/consent-ui/src/app/ais/wait-for-decoupled/wait-for-decoupled.ts @@ -0,0 +1,64 @@ +import {Component, OnInit} from '@angular/core'; +import {ActivatedRoute} from "@angular/router"; +import {ApiHeaders} from "../../api/api.headers"; +import {SessionService} from "../../common/session.service"; +import { + AuthStateConsentAuthorizationService, + ConsentAuth, + PsuAuthRequest, + UpdateConsentAuthorizationService +} from '../../api'; +import {StubUtil} from '../../common/utils/stub-util'; +import {delay, repeatWhen, shareReplay, single, switchMap, takeUntil, tap} from "rxjs/operators"; +import {interval, Observable, of, Subject} from "rxjs"; + +@Component({ + selector: 'wait-for-decoupled-redirection', + templateUrl: './wait-for-decoupled.html', + styleUrls: ['./wait-for-decoupled.scss'] +}) +export class WaitForDecoupled implements OnInit { + public static ROUTE = 'wait-sca-finalization'; + + private readonly POLLING_DELAY_MS = 3000; + + authResponse: ConsentAuth | undefined; + + private authId: string; + + private decoupledCompleted = new Subject(); + + constructor( + private consentAuthorizationService: UpdateConsentAuthorizationService, + private consentStatusService: AuthStateConsentAuthorizationService, + private sessionService: SessionService, + private activatedRoute: ActivatedRoute + ) { + const route = this.activatedRoute.snapshot; + this.authId = route.parent.params.authId; + this.sessionService.setRedirectCode(this.authId, route.queryParams.redirectCode); + } + + ngOnInit() { + of(true) + .pipe(switchMap(_ => this.consentAuthorizationService.embeddedUsingPOST( + this.authId, + StubUtil.X_REQUEST_ID, + this.sessionService.getRedirectCode(this.authId), + {} as PsuAuthRequest, + 'response' + ))) + .pipe(repeatWhen(completed => completed.pipe(delay(this.POLLING_DELAY_MS))), tap()) + .pipe(takeUntil(this.decoupledCompleted)) + .subscribe(res => { + if (res.headers.get(ApiHeaders.X_XSRF_TOKEN)) { + this.sessionService.setRedirectCode(this.authId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); + } + this.authResponse = res.body; + + if (res.status === 202) { + window.location.href = res.headers.get(ApiHeaders.LOCATION); + } + }); + } +} diff --git a/consent-ui/src/app/api-auth/.openapi-generator/VERSION b/consent-ui/src/app/api-auth/.openapi-generator/VERSION index 078bf8b7dd..ecedc98d1d 100644 --- a/consent-ui/src/app/api-auth/.openapi-generator/VERSION +++ b/consent-ui/src/app/api-auth/.openapi-generator/VERSION @@ -1 +1 @@ -4.2.2 \ No newline at end of file +4.3.1 \ No newline at end of file diff --git a/consent-ui/src/app/api-auth/README.md b/consent-ui/src/app/api-auth/README.md index 6fdf5e362f..e0c9192e33 100644 --- a/consent-ui/src/app/api-auth/README.md +++ b/consent-ui/src/app/api-auth/README.md @@ -92,6 +92,31 @@ export function apiConfigFactory (): Configuration => { export class AppModule {} ``` +``` +// configuring providers with an authentication service that manages your access tokens +import { ApiModule, Configuration } from ''; + +@NgModule({ + imports: [ ApiModule ], + declarations: [ AppComponent ], + providers: [ + { + provide: Configuration, + useFactory: (authService: AuthService) => new Configuration( + { + basePath: environment.apiUrl, + accessToken: authService.getAccessToken.bind(authService) + } + ), + deps: [AuthService], + multi: false + } + ], + bootstrap: [ AppComponent ] +}) +export class AppModule {} +``` + ``` import { DefaultApi } from ''; diff --git a/consent-ui/src/app/api-auth/api.module.ts b/consent-ui/src/app/api-auth/api.module.ts index 32aa339db8..322fbd1a03 100644 --- a/consent-ui/src/app/api-auth/api.module.ts +++ b/consent-ui/src/app/api-auth/api.module.ts @@ -10,12 +10,10 @@ import { PsuAuthenticationAndConsentApprovalService } from './api/psuAuthenticat imports: [], declarations: [], exports: [], - providers: [ - PsuAuthenticationService, - PsuAuthenticationAndConsentApprovalService ] + providers: [] }) export class ApiModule { - public static forRoot(configurationFactory: () => Configuration): ModuleWithProviders { + public static forRoot(configurationFactory: () => Configuration): ModuleWithProviders { return { ngModule: ApiModule, providers: [ { provide: Configuration, useFactory: configurationFactory } ] diff --git a/consent-ui/src/app/api-auth/api/psuAuthentication.service.ts b/consent-ui/src/app/api-auth/api/psuAuthentication.service.ts index 80a046ee72..c9e8e2571d 100644 --- a/consent-ui/src/app/api-auth/api/psuAuthentication.service.ts +++ b/consent-ui/src/app/api-auth/api/psuAuthentication.service.ts @@ -17,9 +17,9 @@ import { HttpClient, HttpHeaders, HttpParams, import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; -import { GeneralError } from '../model/generalError'; -import { LoginResponse } from '../model/loginResponse'; -import { PsuAuthBody } from '../model/psuAuthBody'; +import { GeneralError } from '../model/models'; +import { LoginResponse } from '../model/models'; +import { PsuAuthBody } from '../model/models'; import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; import { Configuration } from '../configuration'; @@ -51,6 +51,42 @@ export class PsuAuthenticationService { + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, + (value as Date).toISOString().substr(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + /** * Login user to open-banking * TBD @@ -59,10 +95,10 @@ export class PsuAuthenticationService { * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public login(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'body', reportProgress?: boolean): Observable; - public login(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'response', reportProgress?: boolean): Observable>; - public login(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'events', reportProgress?: boolean): Observable>; - public login(xRequestID: string, psuAuthBody: PsuAuthBody, observe: any = 'body', reportProgress: boolean = false ): Observable { + public login(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public login(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public login(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public login(xRequestID: string, psuAuthBody: PsuAuthBody, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling login.'); } @@ -75,11 +111,14 @@ export class PsuAuthenticationService { headers = headers.set('X-Request-ID', String(xRequestID)); } - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } @@ -94,9 +133,15 @@ export class PsuAuthenticationService { headers = headers.set('Content-Type', httpContentTypeSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.post(`${this.configuration.basePath}/v1/psu/login`, psuAuthBody, { + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, @@ -113,10 +158,10 @@ export class PsuAuthenticationService { * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public registration(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'body', reportProgress?: boolean): Observable; - public registration(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'response', reportProgress?: boolean): Observable>; - public registration(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'events', reportProgress?: boolean): Observable>; - public registration(xRequestID: string, psuAuthBody: PsuAuthBody, observe: any = 'body', reportProgress: boolean = false ): Observable { + public registration(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public registration(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public registration(xRequestID: string, psuAuthBody: PsuAuthBody, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public registration(xRequestID: string, psuAuthBody: PsuAuthBody, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling registration.'); } @@ -129,11 +174,14 @@ export class PsuAuthenticationService { headers = headers.set('X-Request-ID', String(xRequestID)); } - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } @@ -148,9 +196,15 @@ export class PsuAuthenticationService { headers = headers.set('Content-Type', httpContentTypeSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.post(`${this.configuration.basePath}/v1/psu/register`, psuAuthBody, { + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, @@ -167,10 +221,10 @@ export class PsuAuthenticationService { * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public renewalAuthorizationSessionKey(xRequestID: string, authorizationId: string, observe?: 'body', reportProgress?: boolean): Observable; - public renewalAuthorizationSessionKey(xRequestID: string, authorizationId: string, observe?: 'response', reportProgress?: boolean): Observable>; - public renewalAuthorizationSessionKey(xRequestID: string, authorizationId: string, observe?: 'events', reportProgress?: boolean): Observable>; - public renewalAuthorizationSessionKey(xRequestID: string, authorizationId: string, observe: any = 'body', reportProgress: boolean = false ): Observable { + public renewalAuthorizationSessionKey(xRequestID: string, authorizationId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public renewalAuthorizationSessionKey(xRequestID: string, authorizationId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public renewalAuthorizationSessionKey(xRequestID: string, authorizationId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public renewalAuthorizationSessionKey(xRequestID: string, authorizationId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling renewalAuthorizationSessionKey.'); } @@ -183,18 +237,27 @@ export class PsuAuthenticationService { headers = headers.set('X-Request-ID', String(xRequestID)); } - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.get(`${this.configuration.basePath}/v1/psu/ais/${encodeURIComponent(String(authorizationId))}/renewal-authorization-session-key`, { + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, diff --git a/consent-ui/src/app/api-auth/api/psuAuthenticationAndConsentApproval.service.ts b/consent-ui/src/app/api-auth/api/psuAuthenticationAndConsentApproval.service.ts index 56f46bb9ad..671854eb6f 100644 --- a/consent-ui/src/app/api-auth/api/psuAuthenticationAndConsentApproval.service.ts +++ b/consent-ui/src/app/api-auth/api/psuAuthenticationAndConsentApproval.service.ts @@ -3,7 +3,7 @@ * This API provides PSU login and registration functionality on TPP side. * * The version of the OpenAPI document: 1.0.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -11,15 +11,19 @@ */ /* tslint:disable:no-unused-variable member-ordering */ -import { Inject, Injectable, Optional } from '@angular/core'; -import { HttpClient, HttpEvent, HttpHeaders, HttpParameterCodec, HttpParams, HttpResponse } from '@angular/common/http'; -import { CustomHttpParameterCodec } from '../encoder'; -import { Observable } from 'rxjs'; -import { LoginResponse } from '../model/loginResponse'; -import { PsuAuthBody } from '../model/psuAuthBody'; +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +import { GeneralError } from '../model/models'; +import { LoginResponse } from '../model/models'; +import { PsuAuthBody } from '../model/models'; + +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; -import { BASE_PATH } from '../variables'; -import { Configuration } from '../configuration'; @Injectable({ @@ -47,32 +51,69 @@ export class PsuAuthenticationAndConsentApprovalService { + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, + (value as Date).toISOString().substr(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + /** * Login user to open-banking to perform payment (anonymous to OPBA) * TBD - * @param xRequestID Unique ID that identifies this request through common workflow. Shall be contained in HTTP Response as well. + * @param xRequestID Unique ID that identifies this request through common workflow. Shall be contained in HTTP Response as well. * @param authorizationId Authorization session ID to approve * @param redirectCode Redirect code that acts as a password protecting FinTech requested consent specification * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public loginForAnonymousPaymentApproval(xRequestID: string, authorizationId: string, redirectCode: string, observe?: 'body', reportProgress?: boolean): Observable; - public loginForAnonymousPaymentApproval(xRequestID: string, authorizationId: string, redirectCode: string, observe?: 'response', reportProgress?: boolean): Observable>; - public loginForAnonymousPaymentApproval(xRequestID: string, authorizationId: string, redirectCode: string, observe?: 'events', reportProgress?: boolean): Observable>; - public loginForAnonymousPaymentApproval(xRequestID: string, authorizationId: string, redirectCode: string, observe: any = 'body', reportProgress: boolean = false ): Observable { + public loginForAnonymousApproval(xRequestID: string, authorizationId: string, redirectCode: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public loginForAnonymousApproval(xRequestID: string, authorizationId: string, redirectCode: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public loginForAnonymousApproval(xRequestID: string, authorizationId: string, redirectCode: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public loginForAnonymousApproval(xRequestID: string, authorizationId: string, redirectCode: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (xRequestID === null || xRequestID === undefined) { - throw new Error('Required parameter xRequestID was null or undefined when calling loginForAnonymousPaymentApproval.'); + throw new Error('Required parameter xRequestID was null or undefined when calling loginForAnonymousApproval.'); } if (authorizationId === null || authorizationId === undefined) { - throw new Error('Required parameter authorizationId was null or undefined when calling loginForAnonymousPaymentApproval.'); + throw new Error('Required parameter authorizationId was null or undefined when calling loginForAnonymousApproval.'); } if (redirectCode === null || redirectCode === undefined) { - throw new Error('Required parameter redirectCode was null or undefined when calling loginForAnonymousPaymentApproval.'); + throw new Error('Required parameter redirectCode was null or undefined when calling loginForAnonymousApproval.'); } let queryParameters = new HttpParams({encoder: this.encoder}); if (redirectCode !== undefined && redirectCode !== null) { - queryParameters = queryParameters.set('redirectCode', redirectCode); + queryParameters = this.addToHttpParams(queryParameters, + redirectCode, 'redirectCode'); } let headers = this.defaultHeaders; @@ -80,20 +121,29 @@ export class PsuAuthenticationAndConsentApprovalService { headers = headers.set('X-Request-ID', String(xRequestID)); } - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } - return this.httpClient.post(`${this.configuration.basePath}/v1/psu/pis/${encodeURIComponent(String(authorizationId))}/anonymous`, + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + + return this.httpClient.post(`${this.configuration.basePath}/v1/psu/${encodeURIComponent(String(authorizationId))}/for-approval/anonymous`, null, { params: queryParameters, + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, @@ -105,17 +155,17 @@ export class PsuAuthenticationAndConsentApprovalService { /** * Login user to open-banking * TBD - * @param xRequestID Unique ID that identifies this request through common workflow. Shall be contained in HTTP Response as well. + * @param xRequestID Unique ID that identifies this request through common workflow. Shall be contained in HTTP Response as well. * @param authorizationId Authorization session ID to approve * @param redirectCode Redirect code that acts as a password protecting FinTech requested consent specification * @param psuAuthBody User credentials object * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public loginForApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe?: 'body', reportProgress?: boolean): Observable; - public loginForApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe?: 'response', reportProgress?: boolean): Observable>; - public loginForApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe?: 'events', reportProgress?: boolean): Observable>; - public loginForApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe: any = 'body', reportProgress: boolean = false ): Observable { + public loginForApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public loginForApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public loginForApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public loginForApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling loginForApproval.'); } @@ -131,7 +181,8 @@ export class PsuAuthenticationAndConsentApprovalService { let queryParameters = new HttpParams({encoder: this.encoder}); if (redirectCode !== undefined && redirectCode !== null) { - queryParameters = queryParameters.set('redirectCode', redirectCode); + queryParameters = this.addToHttpParams(queryParameters, + redirectCode, 'redirectCode'); } let headers = this.defaultHeaders; @@ -139,11 +190,14 @@ export class PsuAuthenticationAndConsentApprovalService { headers = headers.set('X-Request-ID', String(xRequestID)); } - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } @@ -158,78 +212,16 @@ export class PsuAuthenticationAndConsentApprovalService { headers = headers.set('Content-Type', httpContentTypeSelected); } - return this.httpClient.post(`${this.configuration.basePath}/v1/psu/ais/${encodeURIComponent(String(authorizationId))}/for-approval/login`, - psuAuthBody, - { - params: queryParameters, - withCredentials: this.configuration.withCredentials, - headers: headers, - observe: observe, - reportProgress: reportProgress - } - ); - } - - /** - * Login user to open-banking to perform payment - * TBD - * @param xRequestID Unique ID that identifies this request through common workflow. Shall be contained in HTTP Response as well. - * @param authorizationId Authorization session ID to approve - * @param redirectCode Redirect code that acts as a password protecting FinTech requested consent specification - * @param psuAuthBody User credentials object - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public loginForPaymentApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe?: 'body', reportProgress?: boolean): Observable; - public loginForPaymentApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe?: 'response', reportProgress?: boolean): Observable>; - public loginForPaymentApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe?: 'events', reportProgress?: boolean): Observable>; - public loginForPaymentApproval(xRequestID: string, authorizationId: string, redirectCode: string, psuAuthBody: PsuAuthBody, observe: any = 'body', reportProgress: boolean = false ): Observable { - if (xRequestID === null || xRequestID === undefined) { - throw new Error('Required parameter xRequestID was null or undefined when calling loginForPaymentApproval.'); - } - if (authorizationId === null || authorizationId === undefined) { - throw new Error('Required parameter authorizationId was null or undefined when calling loginForPaymentApproval.'); - } - if (redirectCode === null || redirectCode === undefined) { - throw new Error('Required parameter redirectCode was null or undefined when calling loginForPaymentApproval.'); - } - if (psuAuthBody === null || psuAuthBody === undefined) { - throw new Error('Required parameter psuAuthBody was null or undefined when calling loginForPaymentApproval.'); - } - - let queryParameters = new HttpParams({encoder: this.encoder}); - if (redirectCode !== undefined && redirectCode !== null) { - queryParameters = queryParameters.set('redirectCode', redirectCode); - } - - let headers = this.defaultHeaders; - if (xRequestID !== undefined && xRequestID !== null) { - headers = headers.set('X-Request-ID', String(xRequestID)); - } - - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); - if (httpHeaderAcceptSelected !== undefined) { - headers = headers.set('Accept', httpHeaderAcceptSelected); - } - - - // to determine the Content-Type header - const consumes: string[] = [ - 'application/json' - ]; - const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); - if (httpContentTypeSelected !== undefined) { - headers = headers.set('Content-Type', httpContentTypeSelected); + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; } - return this.httpClient.post(`${this.configuration.basePath}/v1/psu/pis/${encodeURIComponent(String(authorizationId))}/for-approval/login`, + return this.httpClient.post(`${this.configuration.basePath}/v1/psu/${encodeURIComponent(String(authorizationId))}/for-approval/login`, psuAuthBody, { params: queryParameters, + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, diff --git a/consent-ui/src/app/api/api.headers.ts b/consent-ui/src/app/api/api.headers.ts index 5454ab05e7..0d40d4085e 100644 --- a/consent-ui/src/app/api/api.headers.ts +++ b/consent-ui/src/app/api/api.headers.ts @@ -1,5 +1,6 @@ export enum ApiHeaders { REDIRECT_CODE = 'Redirect-Code', + X_XSRF_TOKEN = 'X-XSRF-TOKEN', LOCATION = 'Location', COOKIE_TTL = 'Cookie-TTL' } diff --git a/consent-ui/src/app/api/api/authStateConsentAuthorization.service.ts b/consent-ui/src/app/api/api/authStateConsentAuthorization.service.ts index a66b0929a1..611d87b531 100644 --- a/consent-ui/src/app/api/api/authStateConsentAuthorization.service.ts +++ b/consent-ui/src/app/api/api/authStateConsentAuthorization.service.ts @@ -88,24 +88,24 @@ export class AuthStateConsentAuthorizationService { /** * Redirect entry point for initiating a consent authorization process. - * This is the <b>entry point</b> for processing a consent redirected by the TppBankingApi to this ConsentAuthorisationApi. At this entry point, the ConsentAuthorisationApi will use the redirectCode to retrieve the RedirectSession from the TppServer. An analysis of the RedirectSession will help decide if the ConsentAuthorisationApi will proceed with an embedded approach (E<sub>1</sub>) or a redirect approach (R<sub>1</sub>). + * This is the <b>entry point</b> for processing a consent redirected by the TppBankingApi to this ConsentAuthorisationApi. At this entry point, the ConsentAuthorisationApi will use the xXsrfToken to retrieve the RedirectSession from the TppServer. An analysis of the RedirectSession will help decide if the ConsentAuthorisationApi will proceed with an embedded approach (E<sub>1</sub>) or a redirect approach (R<sub>1</sub>). * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. - * @param redirectCode Code used to retrieve a redirect session. This is generaly transported as a query parameter + * @param xXsrfToken XSRF parameter used to retrieve a redirect session. This is generaly transported as a query parameter. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public authUsingGET(authId: string, redirectCode?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; - public authUsingGET(authId: string, redirectCode?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public authUsingGET(authId: string, redirectCode?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public authUsingGET(authId: string, redirectCode?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { + public authUsingGET(authId: string, xXsrfToken?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public authUsingGET(authId: string, xXsrfToken?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public authUsingGET(authId: string, xXsrfToken?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public authUsingGET(authId: string, xXsrfToken?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (authId === null || authId === undefined) { throw new Error('Required parameter authId was null or undefined when calling authUsingGET.'); } let queryParameters = new HttpParams({encoder: this.encoder}); - if (redirectCode !== undefined && redirectCode !== null) { + if (xXsrfToken !== undefined && xXsrfToken !== null) { queryParameters = this.addToHttpParams(queryParameters, - redirectCode, 'redirectCode'); + xXsrfToken, 'xXsrfToken'); } let headers = this.defaultHeaders; diff --git a/consent-ui/src/app/api/api/consentAuthorization.service.ts b/consent-ui/src/app/api/api/consentAuthorization.service.ts deleted file mode 100644 index 271238c543..0000000000 --- a/consent-ui/src/app/api/api/consentAuthorization.service.ts +++ /dev/null @@ -1,400 +0,0 @@ -/** - * Open Banking Gateway - Consent Authorization API. - * Interface used by the PsuUserAgent to present consent authorization services to the PSU. The consent authorization process is triggered by redirecting the PSU from the [TppBankingApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#TppBankingApi) (2a) over the [FinTechApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#FinTechApi) (2b) to the /consent/{auth-id} entry point of this [ConsentAuthorisationApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#ConsentAuthorisationApi) (2c). The decision on whether the authorization process is embedded or redirected is taken by this ConsentAuthorisationApi. The following picture displays the overall architecture of this open banking consent authorisation api: ![High level architecture](/img/open-banking-consent-authorisation-api.png) #### User Agent This Api assumes that the PsuUserAgent is a modern browsers that : * automatically detects the \"302 Found\" response code and proceeds with the associated location url, * stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). This Api also assumes any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of amodern browser with respect to 30X and Cookies. #### Redirecting to the ConsentAuthorisationApi (2a) Any service request of the FinTechUI to the FinTechApi (1a) will be forwarded to the TppBankingApi (1b). This forward might contain a [PsuConsentSession](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#PsuConsentSession) that is used to identify the PSU in the world of the TPP. The TppBankingApi uses the provided PsuConsentSession to retrieve an eventualy suitable consent that will be used to forward the corresponding service request to the OpenBankingApi (1c) of the ASPSP. If there is no suitable consent, the TPP might still send a consent initiation request to the OpenBankingApi (1c). Whether this request is sent or not depends on the design of the target OpenBankingApi. Finally, the TppBankingApi will if necessary instruct the FinTechApi (2a) to redirect the PsuUgerAgent (2b) to the /consent/{auth-id} entry point of the ConsentAuthorisationApi (2c). #### Issolation Authorisation Request Processing The auth-id parameter is used to make sure paralell authorization requests are not mixup. #### SessionCookies and XSRF Each authorisation session started will be associated with a proper SessionCookie and a corresponding XSRF-TOKEN. * The request that sets a session cookie (E1) also add the X-XSRF-TOKEN to the response header. * The cookie path is always extended with the corresponding auth-id, so two Authorization processes can not share state. * Each authenticated request sent to the ConsentAuthorisationApi will provide the X-XSRF-TOKEN matching the sent SessionCookie. #### RedirectCookie and XSRF (R1) In a redirect approach (Redirecting PSU to the ASPSP), the The retruned AuthorizeResponse object contains information needed to present a suitable redirect info page to the PSU. Redirection can either be actively performed by the UIApplication or performed as a result of a 30x redirect response to the PsuUserAgent. In both cases, a RedirectCookie will be associated with the PsuUserAgent and a corresponding XSRF-TOKEN named redirectState will be addedto the back redirect url. #### Final Result of the Authorization Process The final result of the authorization process is a PsuCosentSession that is returned by the token endpoint of the TppBankingAPi to the FinTechApi (4c). This handle will (PsuCosentSession) will be stored by the FinTechApi and added a PSU identifying information to each service request associated with this PSU. - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -/* tslint:disable:no-unused-variable member-ordering */ - -import { Inject, Injectable, Optional } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpParams, - HttpResponse, HttpEvent, HttpParameterCodec } from '@angular/common/http'; -import { CustomHttpParameterCodec } from '../encoder'; -import { Observable } from 'rxjs'; - -import { AuthorizeRequest } from '../model/authorizeRequest'; -import { DenyRequest } from '../model/denyRequest'; -import { InlineResponse200 } from '../model/inlineResponse200'; -import { PsuAuthRequest } from '../model/psuAuthRequest'; -import { PsuMessage } from '../model/psuMessage'; - -import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; -import { Configuration } from '../configuration'; - - - -@Injectable({ - providedIn: 'root' -}) -export class ConsentAuthorizationService { - - protected basePath = 'http://localhost'; - public defaultHeaders = new HttpHeaders(); - public configuration = new Configuration(); - public encoder: HttpParameterCodec; - - constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string, @Optional() configuration: Configuration) { - if (configuration) { - this.configuration = configuration; - } - if (typeof this.configuration.basePath !== 'string') { - if (typeof basePath !== 'string') { - basePath = this.basePath; - } - this.configuration.basePath = basePath; - } - this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); - } - - - - /** - * Redirect entry point for initiating a consent authorization process. - * This is the <b>entry point</b> for processing a consent redirected by the TppBankingApi to this ConsentAuthorisationApi. At this entry point, the ConsentAuthorisationApi will use the redirectCode to retrieve the RedirectSession from the TppServer. An analysis of the RedirectSession will help decide if the ConsentAuthorisationApi will proceed with an embedded approach (E<sub>1</sub>) or a redirect approach (R<sub>1</sub>). - * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. - * @param redirectCode Code used to retrieve a redirect session. This is generaly transported as a query parameter - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public authUsingGET(authId: string, redirectCode?: string, observe?: 'body', reportProgress?: boolean): Observable; - public authUsingGET(authId: string, redirectCode?: string, observe?: 'response', reportProgress?: boolean): Observable>; - public authUsingGET(authId: string, redirectCode?: string, observe?: 'events', reportProgress?: boolean): Observable>; - public authUsingGET(authId: string, redirectCode?: string, observe: any = 'body', reportProgress: boolean = false ): Observable { - if (authId === null || authId === undefined) { - throw new Error('Required parameter authId was null or undefined when calling authUsingGET.'); - } - - let queryParameters = new HttpParams({encoder: this.encoder}); - if (redirectCode !== undefined && redirectCode !== null) { - queryParameters = queryParameters.set('redirectCode', redirectCode); - } - - let headers = this.defaultHeaders; - - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); - if (httpHeaderAcceptSelected !== undefined) { - headers = headers.set('Accept', httpHeaderAcceptSelected); - } - - - return this.httpClient.get(`${this.configuration.basePath}/v1/consent/${encodeURIComponent(String(authId))}`, - { - params: queryParameters, - withCredentials: this.configuration.withCredentials, - headers: headers, - observe: observe, - reportProgress: reportProgress - } - ); - } - - /** - * Consent authorization is denied - consent is blocked. Closes this session and redirects the PSU back to the FinTechApi or close the application window. - * Closes this session and redirects the PSU back to the FinTechApi or close the application window. In any case, the session of the user will be closed and cookies will be deleted with the response to this request. - * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. - * @param xRequestID Unique ID that identifies this request through common workflow. Shall be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie. The token matches the auth-id included in the requestpath and prefixing the cookie. - * @param denyRequest - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public denyUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, denyRequest: DenyRequest, observe?: 'body', reportProgress?: boolean): Observable; - public denyUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, denyRequest: DenyRequest, observe?: 'response', reportProgress?: boolean): Observable>; - public denyUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, denyRequest: DenyRequest, observe?: 'events', reportProgress?: boolean): Observable>; - public denyUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, denyRequest: DenyRequest, observe: any = 'body', reportProgress: boolean = false ): Observable { - if (authId === null || authId === undefined) { - throw new Error('Required parameter authId was null or undefined when calling denyUsingPOST.'); - } - if (xRequestID === null || xRequestID === undefined) { - throw new Error('Required parameter xRequestID was null or undefined when calling denyUsingPOST.'); - } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling denyUsingPOST.'); - } - if (denyRequest === null || denyRequest === undefined) { - throw new Error('Required parameter denyRequest was null or undefined when calling denyUsingPOST.'); - } - - let headers = this.defaultHeaders; - if (xRequestID !== undefined && xRequestID !== null) { - headers = headers.set('X-Request-ID', String(xRequestID)); - } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); - } - - // authentication (sessionCookie) required - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); - if (httpHeaderAcceptSelected !== undefined) { - headers = headers.set('Accept', httpHeaderAcceptSelected); - } - - - // to determine the Content-Type header - const consumes: string[] = [ - 'application/json' - ]; - const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); - if (httpContentTypeSelected !== undefined) { - headers = headers.set('Content-Type', httpContentTypeSelected); - } - - return this.httpClient.post(`${this.configuration.basePath}/v1/consent/${encodeURIComponent(String(authId))}/deny`, - denyRequest, - { - withCredentials: this.configuration.withCredentials, - headers: headers, - observe: observe, - reportProgress: reportProgress - } - ); - } - - /** - * Generic challenge response end point for updating consent session with PSU authentication data while requesting remaining challenges for the ongoing authorization process. - * Update consent session with PSU auth data whereby requesting remaining challenges for the ongoing authorization process. Returns 202 if one should proceed to some other link. Link to follow is in \'Location\' header. - * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. - * @param xRequestID Unique ID that identifies this request through common workflow. Shall be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie. The token matches the auth-id included in the requestpath and prefixing the cookie. - * @param redirectCode Code used to retrieve a redirect session. This is generaly transported as a query parameter - * @param psuAuthRequest - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public embeddedUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, redirectCode?: string, psuAuthRequest?: PsuAuthRequest, observe?: 'body', reportProgress?: boolean): Observable; - public embeddedUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, redirectCode?: string, psuAuthRequest?: PsuAuthRequest, observe?: 'response', reportProgress?: boolean): Observable>; - public embeddedUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, redirectCode?: string, psuAuthRequest?: PsuAuthRequest, observe?: 'events', reportProgress?: boolean): Observable>; - public embeddedUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, redirectCode?: string, psuAuthRequest?: PsuAuthRequest, observe: any = 'body', reportProgress: boolean = false ): Observable { - if (authId === null || authId === undefined) { - throw new Error('Required parameter authId was null or undefined when calling embeddedUsingPOST.'); - } - if (xRequestID === null || xRequestID === undefined) { - throw new Error('Required parameter xRequestID was null or undefined when calling embeddedUsingPOST.'); - } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling embeddedUsingPOST.'); - } - - let queryParameters = new HttpParams({encoder: this.encoder}); - if (redirectCode !== undefined && redirectCode !== null) { - queryParameters = queryParameters.set('redirectCode', redirectCode); - } - - let headers = this.defaultHeaders; - if (xRequestID !== undefined && xRequestID !== null) { - headers = headers.set('X-Request-ID', String(xRequestID)); - } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); - } - - // authentication (sessionCookie) required - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); - if (httpHeaderAcceptSelected !== undefined) { - headers = headers.set('Accept', httpHeaderAcceptSelected); - } - - - // to determine the Content-Type header - const consumes: string[] = [ - 'application/json' - ]; - const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); - if (httpContentTypeSelected !== undefined) { - headers = headers.set('Content-Type', httpContentTypeSelected); - } - - return this.httpClient.post(`${this.configuration.basePath}/v1/consent/${encodeURIComponent(String(authId))}/embedded`, - psuAuthRequest, - { - params: queryParameters, - withCredentials: this.configuration.withCredentials, - headers: headers, - observe: observe, - reportProgress: reportProgress - } - ); - } - - /** - * Redirecting back from ASPSP to TPP after a failed consent authorization. - * Redirecting back from ASPSP to TPP after a failed consent authorization. In any case, the corresponding redirect session of the user will be closed and cookies will be deleted with the response to this request. - * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. - * @param redirectState XSRF parameter used to validate an RedirectCookie. This is generaly transported as a path parameter. - * @param redirectCode Code used to retrieve a redirect session. This is generaly transported as a query parameter - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public fromAspspNokUsingGET(authId: string, redirectState: string, redirectCode?: string, observe?: 'body', reportProgress?: boolean): Observable; - public fromAspspNokUsingGET(authId: string, redirectState: string, redirectCode?: string, observe?: 'response', reportProgress?: boolean): Observable>; - public fromAspspNokUsingGET(authId: string, redirectState: string, redirectCode?: string, observe?: 'events', reportProgress?: boolean): Observable>; - public fromAspspNokUsingGET(authId: string, redirectState: string, redirectCode?: string, observe: any = 'body', reportProgress: boolean = false ): Observable { - if (authId === null || authId === undefined) { - throw new Error('Required parameter authId was null or undefined when calling fromAspspNokUsingGET.'); - } - if (redirectState === null || redirectState === undefined) { - throw new Error('Required parameter redirectState was null or undefined when calling fromAspspNokUsingGET.'); - } - - let queryParameters = new HttpParams({encoder: this.encoder}); - if (redirectCode !== undefined && redirectCode !== null) { - queryParameters = queryParameters.set('redirectCode', redirectCode); - } - - let headers = this.defaultHeaders; - - // authentication (redirectCookie) required - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); - if (httpHeaderAcceptSelected !== undefined) { - headers = headers.set('Accept', httpHeaderAcceptSelected); - } - - - return this.httpClient.get(`${this.configuration.basePath}/v1/consent/${encodeURIComponent(String(authId))}/fromAspsp/${encodeURIComponent(String(redirectState))}/nok`, - { - params: queryParameters, - withCredentials: this.configuration.withCredentials, - headers: headers, - observe: observe, - reportProgress: reportProgress - } - ); - } - - /** - * Redirecting back from ASPSP to ConsentAuthorisationApi after a successful consent authorization. - * Redirecting back from ASPSP to ConsentAuthorisationApi after a successful consent authorization. In any case, the corresponding redirect session of the user will be closed and cookies will be deleted with the response to this request. ##### Desiging the BackRedirectURL (R<sub>6</sub>) The BackRedirectURL (OkUrl, NokUrl, etc... depending of ASPSP API) is the URL used by the ASPSP to send the PsuUserAgent back to the ConsentAuthorisationApi. Event though the structure of this URL might be constrained by the nature of the ASPSP OpenBankingApi, the BackRedirectURL must contains atleast : * A redirect-id (as a path parameter) used to isolate many redirect processes form each order. * A consentAuthState (as a path or query parameter) used to protect the TppConsentSessionCookie as a XSRF parameter. * The consentAuthState might if necessary be used to encrypt the attached ConsentAuthSessionCookie. ##### Back-Redirecting PSU to the FinTechApi (4<sub>b</sub>) Prior to redirecting the PSU back to the FinTechApi, consent information will be stored by the ConsentAuthorisationApi in a RedirectSession as well. * The one time resulting redirectCode will be attached as a query parameter to the location URL leading back to the FinTechApi. * After verifying the FinTechRedirectSessionCookie (4<sub>b</sub>), the FinTechApi must forward this redirectCode to the token endpoint of the TppBankingAPi (4<sub>c</sub>). * The TppBankingApi will then retrieve the RedirectSession using the redirectCode and proceed forward with the authorization process. - * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. - * @param redirectState XSRF parameter used to validate an RedirectCookie. This is generaly transported as a path parameter. - * @param redirectCode Code used to retrieve a redirect session. This is generaly transported as a query parameter - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public fromAspspOkUsingGET(authId: string, redirectState: string, redirectCode?: string, observe?: 'body', reportProgress?: boolean): Observable; - public fromAspspOkUsingGET(authId: string, redirectState: string, redirectCode?: string, observe?: 'response', reportProgress?: boolean): Observable>; - public fromAspspOkUsingGET(authId: string, redirectState: string, redirectCode?: string, observe?: 'events', reportProgress?: boolean): Observable>; - public fromAspspOkUsingGET(authId: string, redirectState: string, redirectCode?: string, observe: any = 'body', reportProgress: boolean = false ): Observable { - if (authId === null || authId === undefined) { - throw new Error('Required parameter authId was null or undefined when calling fromAspspOkUsingGET.'); - } - if (redirectState === null || redirectState === undefined) { - throw new Error('Required parameter redirectState was null or undefined when calling fromAspspOkUsingGET.'); - } - - let queryParameters = new HttpParams({encoder: this.encoder}); - if (redirectCode !== undefined && redirectCode !== null) { - queryParameters = queryParameters.set('redirectCode', redirectCode); - } - - let headers = this.defaultHeaders; - - // authentication (redirectCookie) required - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); - if (httpHeaderAcceptSelected !== undefined) { - headers = headers.set('Accept', httpHeaderAcceptSelected); - } - - - return this.httpClient.get(`${this.configuration.basePath}/v1/consent/${encodeURIComponent(String(authId))}/fromAspsp/${encodeURIComponent(String(redirectState))}/ok`, - { - params: queryParameters, - withCredentials: this.configuration.withCredentials, - headers: headers, - observe: observe, - reportProgress: reportProgress - } - ); - } - - /** - * Provides the ConsentAuthorisationApi with the opportunity to redirect the PSU to the ASPSP. - * Provides the ConsentAuthorisationApi with the opportunity to redirect the PSU to the ASPSP. - * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. - * @param xRequestID Unique ID that identifies this request through common workflow. Shall be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie. The token matches the auth-id included in the requestpath and prefixing the cookie. - * @param authorizeRequest - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public toAspspGrantUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, authorizeRequest: AuthorizeRequest, observe?: 'body', reportProgress?: boolean): Observable; - public toAspspGrantUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, authorizeRequest: AuthorizeRequest, observe?: 'response', reportProgress?: boolean): Observable>; - public toAspspGrantUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, authorizeRequest: AuthorizeRequest, observe?: 'events', reportProgress?: boolean): Observable>; - public toAspspGrantUsingPOST(authId: string, xRequestID: string, X_XSRF_TOKEN: string, authorizeRequest: AuthorizeRequest, observe: any = 'body', reportProgress: boolean = false ): Observable { - if (authId === null || authId === undefined) { - throw new Error('Required parameter authId was null or undefined when calling toAspspGrantUsingPOST.'); - } - if (xRequestID === null || xRequestID === undefined) { - throw new Error('Required parameter xRequestID was null or undefined when calling toAspspGrantUsingPOST.'); - } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling toAspspGrantUsingPOST.'); - } - if (authorizeRequest === null || authorizeRequest === undefined) { - throw new Error('Required parameter authorizeRequest was null or undefined when calling toAspspGrantUsingPOST.'); - } - - let headers = this.defaultHeaders; - if (xRequestID !== undefined && xRequestID !== null) { - headers = headers.set('X-Request-ID', String(xRequestID)); - } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); - } - - // authentication (sessionCookie) required - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); - if (httpHeaderAcceptSelected !== undefined) { - headers = headers.set('Accept', httpHeaderAcceptSelected); - } - - - // to determine the Content-Type header - const consumes: string[] = [ - 'application/json' - ]; - const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); - if (httpContentTypeSelected !== undefined) { - headers = headers.set('Content-Type', httpContentTypeSelected); - } - - return this.httpClient.post(`${this.configuration.basePath}/v1/consent/${encodeURIComponent(String(authId))}/toAspsp/grant`, - authorizeRequest, - { - withCredentials: this.configuration.withCredentials, - headers: headers, - observe: observe, - reportProgress: reportProgress - } - ); - } - -} diff --git a/consent-ui/src/app/api/api/fromASPSPConsentAuthorization.service.ts b/consent-ui/src/app/api/api/fromASPSPConsentAuthorization.service.ts index b8eb908f94..c9c5aae285 100644 --- a/consent-ui/src/app/api/api/fromASPSPConsentAuthorization.service.ts +++ b/consent-ui/src/app/api/api/fromASPSPConsentAuthorization.service.ts @@ -90,15 +90,14 @@ export class FromASPSPConsentAuthorizationService { * Redirecting back from ASPSP to TPP after a failed consent authorization. * Redirecting back from ASPSP to TPP after a failed consent authorization. In any case, the corresponding redirect session of the user will be closed and cookies will be deleted with the response to this request. * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. - * @param redirectState XSRF parameter used to validate an RedirectCookie. This is generaly transported as a path parameter. - * @param redirectCode Code used to retrieve a redirect session. This is generaly transported as a query parameter + * @param redirectState Code used to retrieve a redirect session. This is generaly transported as a path parameter due to some banks limitiations (ING ASPSP) instead of being transported as query parameter * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public fromAspspNokUsingGET(authId: string, redirectState: string, redirectCode?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; - public fromAspspNokUsingGET(authId: string, redirectState: string, redirectCode?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public fromAspspNokUsingGET(authId: string, redirectState: string, redirectCode?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public fromAspspNokUsingGET(authId: string, redirectState: string, redirectCode?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { + public fromAspspNokUsingGET(authId: string, redirectState: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public fromAspspNokUsingGET(authId: string, redirectState: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public fromAspspNokUsingGET(authId: string, redirectState: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public fromAspspNokUsingGET(authId: string, redirectState: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (authId === null || authId === undefined) { throw new Error('Required parameter authId was null or undefined when calling fromAspspNokUsingGET.'); } @@ -106,12 +105,6 @@ export class FromASPSPConsentAuthorizationService { throw new Error('Required parameter redirectState was null or undefined when calling fromAspspNokUsingGET.'); } - let queryParameters = new HttpParams({encoder: this.encoder}); - if (redirectCode !== undefined && redirectCode !== null) { - queryParameters = this.addToHttpParams(queryParameters, - redirectCode, 'redirectCode'); - } - let headers = this.defaultHeaders; // authentication (redirectCookie) required @@ -141,7 +134,6 @@ export class FromASPSPConsentAuthorizationService { return this.httpClient.get(`${this.configuration.basePath}/v1/consent/${encodeURIComponent(String(authId))}/fromAspsp/${encodeURIComponent(String(redirectState))}/nok`, { - params: queryParameters, responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, @@ -153,18 +145,17 @@ export class FromASPSPConsentAuthorizationService { /** * Redirecting back from ASPSP to ConsentAuthorisationApi after a successful consent authorization. - * Redirecting back from ASPSP to ConsentAuthorisationApi after a successful consent authorization. In any case, the corresponding redirect session of the user will be closed and cookies will be deleted with the response to this request. ##### Desiging the BackRedirectURL (R<sub>6</sub>) The BackRedirectURL (OkUrl, NokUrl, etc... depending of ASPSP API) is the URL used by the ASPSP to send the PsuUserAgent back to the ConsentAuthorisationApi. Event though the structure of this URL might be constrained by the nature of the ASPSP OpenBankingApi, the BackRedirectURL must contains atleast : * A redirect-id (as a path parameter) used to isolate many redirect processes form each order. * A consentAuthState (as a path or query parameter) used to protect the TppConsentSessionCookie as a XSRF parameter. * The consentAuthState might if necessary be used to encrypt the attached ConsentAuthSessionCookie. ##### Back-Redirecting PSU to the FinTechApi (4<sub>b</sub>) Prior to redirecting the PSU back to the FinTechApi, consent information will be stored by the ConsentAuthorisationApi in a RedirectSession as well. * The one time resulting redirectCode will be attached as a query parameter to the location URL leading back to the FinTechApi. * After verifying the FinTechRedirectSessionCookie (4<sub>b</sub>), the FinTechApi must forward this redirectCode to the token endpoint of the TppBankingAPi (4<sub>c</sub>). * The TppBankingApi will then retrieve the RedirectSession using the redirectCode and proceed forward with the authorization process. + * Redirecting back from ASPSP to ConsentAuthorisationApi after a successful consent authorization. In any case, the corresponding redirect session of the user will be closed and cookies will be deleted with the response to this request. ##### Desiging the BackRedirectURL (R<sub>6</sub>) The BackRedirectURL (OkUrl, NokUrl, etc... depending of ASPSP API) is the URL used by the ASPSP to send the PsuUserAgent back to the ConsentAuthorisationApi. Event though the structure of this URL might be constrained by the nature of the ASPSP OpenBankingApi, the BackRedirectURL must contains atleast : * A redirect-id (as a path parameter) used to isolate many redirect processes form each order. * A consentAuthState (as a path or query parameter) used to protect the TppConsentSessionCookie as a XSRF parameter. * The consentAuthState might if necessary be used to encrypt the attached ConsentAuthSessionCookie. ##### Back-Redirecting PSU to the FinTechApi (4<sub>b</sub>) Prior to redirecting the PSU back to the FinTechApi, consent information will be stored by the ConsentAuthorisationApi in a RedirectSession as well. * The one time resulting xXsrfToken will be attached as a query parameter to the location URL leading back to the FinTechApi. * After verifying the FinTechRedirectSessionCookie (4<sub>b</sub>), the FinTechApi must forward this xXsrfToken to the token endpoint of the TppBankingAPi (4<sub>c</sub>). * The TppBankingApi will then retrieve the RedirectSession using the xXsrfToken and proceed forward with the authorization process. * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. - * @param redirectState XSRF parameter used to validate an RedirectCookie. This is generaly transported as a path parameter. - * @param redirectCode Code used to retrieve a redirect session. This is generaly transported as a query parameter + * @param redirectState Code used to retrieve a redirect session. This is generaly transported as a path parameter due to some banks limitiations (ING ASPSP) instead of being transported as query parameter * @param code Oauth2 code to exchange for token. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public fromAspspOkUsingGET(authId: string, redirectState: string, redirectCode?: string, code?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; - public fromAspspOkUsingGET(authId: string, redirectState: string, redirectCode?: string, code?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public fromAspspOkUsingGET(authId: string, redirectState: string, redirectCode?: string, code?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public fromAspspOkUsingGET(authId: string, redirectState: string, redirectCode?: string, code?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { + public fromAspspOkUsingGET(authId: string, redirectState: string, code?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public fromAspspOkUsingGET(authId: string, redirectState: string, code?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public fromAspspOkUsingGET(authId: string, redirectState: string, code?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public fromAspspOkUsingGET(authId: string, redirectState: string, code?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (authId === null || authId === undefined) { throw new Error('Required parameter authId was null or undefined when calling fromAspspOkUsingGET.'); } @@ -173,10 +164,6 @@ export class FromASPSPConsentAuthorizationService { } let queryParameters = new HttpParams({encoder: this.encoder}); - if (redirectCode !== undefined && redirectCode !== null) { - queryParameters = this.addToHttpParams(queryParameters, - redirectCode, 'redirectCode'); - } if (code !== undefined && code !== null) { queryParameters = this.addToHttpParams(queryParameters, code, 'code'); diff --git a/consent-ui/src/app/api/api/updateConsentAuthorization.service.ts b/consent-ui/src/app/api/api/updateConsentAuthorization.service.ts index 001c047c76..0722bbb161 100644 --- a/consent-ui/src/app/api/api/updateConsentAuthorization.service.ts +++ b/consent-ui/src/app/api/api/updateConsentAuthorization.service.ts @@ -17,9 +17,7 @@ import { HttpClient, HttpHeaders, HttpParams, import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; -import { AuthorizeRequest } from '../model/models'; import { ConsentAuth } from '../model/models'; -import { DenyRequest } from '../model/models'; import { PsuAuthRequest } from '../model/models'; import { PsuMessage } from '../model/models'; @@ -94,35 +92,24 @@ export class UpdateConsentAuthorizationService { * Closes this session and redirects the PSU back to the FinTechApi or close the application window. In any case, the session of the user will be closed and cookies will be deleted with the response to this request. * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. * @param xRequestID Unique ID that identifies this request through common workflow. Shall be contained in HTTP Response as well. - * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie. The token matches the auth-id included in the requestpath and prefixing the cookie. - * @param denyRequest * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public denyUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, denyRequest: DenyRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; - public denyUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, denyRequest: DenyRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public denyUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, denyRequest: DenyRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public denyUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, denyRequest: DenyRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { + public denyUsingPOST(authId: string, xRequestID: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public denyUsingPOST(authId: string, xRequestID: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public denyUsingPOST(authId: string, xRequestID: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public denyUsingPOST(authId: string, xRequestID: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (authId === null || authId === undefined) { throw new Error('Required parameter authId was null or undefined when calling denyUsingPOST.'); } if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling denyUsingPOST.'); } - if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { - throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling denyUsingPOST.'); - } - if (denyRequest === null || denyRequest === undefined) { - throw new Error('Required parameter denyRequest was null or undefined when calling denyUsingPOST.'); - } let headers = this.defaultHeaders; if (xRequestID !== undefined && xRequestID !== null) { headers = headers.set('X-Request-ID', String(xRequestID)); } - if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); - } // authentication (sessionCookie) required if (this.configuration.apiKeys) { @@ -144,22 +131,13 @@ export class UpdateConsentAuthorizationService { } - // to determine the Content-Type header - const consumes: string[] = [ - 'application/json' - ]; - const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); - if (httpContentTypeSelected !== undefined) { - headers = headers.set('Content-Type', httpContentTypeSelected); - } - let responseType: 'text' | 'json' = 'json'; if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { responseType = 'text'; } return this.httpClient.post(`${this.configuration.basePath}/v1/consent/${encodeURIComponent(String(authId))}/deny`, - denyRequest, + null, { responseType: responseType, withCredentials: this.configuration.withCredentials, @@ -175,39 +153,32 @@ export class UpdateConsentAuthorizationService { * Update consent session with PSU auth data whereby requesting remaining challenges for the ongoing authorization process. Returns 202 if one should proceed to some other link. Link to follow is in \'Location\' header. * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. * @param xRequestID Unique ID that identifies this request through common workflow. Shall be contained in HTTP Response as well. - * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie. The token matches the auth-id included in the requestpath and prefixing the cookie. - * @param redirectCode Code used to retrieve a redirect session. This is generaly transported as a query parameter + * @param xXsrfToken XSRF parameter used to retrieve a redirect session. This is generaly transported as a query parameter. * @param psuAuthRequest * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public embeddedUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, redirectCode?: string, psuAuthRequest?: PsuAuthRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; - public embeddedUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, redirectCode?: string, psuAuthRequest?: PsuAuthRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public embeddedUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, redirectCode?: string, psuAuthRequest?: PsuAuthRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public embeddedUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, redirectCode?: string, psuAuthRequest?: PsuAuthRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { + public embeddedUsingPOST(authId: string, xRequestID: string, xXsrfToken?: string, psuAuthRequest?: PsuAuthRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public embeddedUsingPOST(authId: string, xRequestID: string, xXsrfToken?: string, psuAuthRequest?: PsuAuthRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public embeddedUsingPOST(authId: string, xRequestID: string, xXsrfToken?: string, psuAuthRequest?: PsuAuthRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public embeddedUsingPOST(authId: string, xRequestID: string, xXsrfToken?: string, psuAuthRequest?: PsuAuthRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (authId === null || authId === undefined) { throw new Error('Required parameter authId was null or undefined when calling embeddedUsingPOST.'); } if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling embeddedUsingPOST.'); } - if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { - throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling embeddedUsingPOST.'); - } let queryParameters = new HttpParams({encoder: this.encoder}); - if (redirectCode !== undefined && redirectCode !== null) { + if (xXsrfToken !== undefined && xXsrfToken !== null) { queryParameters = this.addToHttpParams(queryParameters, - redirectCode, 'redirectCode'); + xXsrfToken, 'xXsrfToken'); } let headers = this.defaultHeaders; if (xRequestID !== undefined && xRequestID !== null) { headers = headers.set('X-Request-ID', String(xRequestID)); } - if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); - } // authentication (sessionCookie) required if (this.configuration.apiKeys) { @@ -256,85 +227,4 @@ export class UpdateConsentAuthorizationService { ); } - /** - * Provides the ConsentAuthorisationApi with the opportunity to redirect the PSU to the ASPSP. - * Provides the ConsentAuthorisationApi with the opportunity to redirect the PSU to the ASPSP. - * @param authId Used to distinguish between different consent authorization processes started by the same PSU. Also included in the corresponding cookie path to limit visibility of the consent cookie to the corresponding consent process. - * @param xRequestID Unique ID that identifies this request through common workflow. Shall be contained in HTTP Response as well. - * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie. The token matches the auth-id included in the requestpath and prefixing the cookie. - * @param authorizeRequest - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public toAspspGrantUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, authorizeRequest: AuthorizeRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; - public toAspspGrantUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, authorizeRequest: AuthorizeRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public toAspspGrantUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, authorizeRequest: AuthorizeRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; - public toAspspGrantUsingPOST(authId: string, xRequestID: string, xXSRFTOKEN: string, authorizeRequest: AuthorizeRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { - if (authId === null || authId === undefined) { - throw new Error('Required parameter authId was null or undefined when calling toAspspGrantUsingPOST.'); - } - if (xRequestID === null || xRequestID === undefined) { - throw new Error('Required parameter xRequestID was null or undefined when calling toAspspGrantUsingPOST.'); - } - if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { - throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling toAspspGrantUsingPOST.'); - } - if (authorizeRequest === null || authorizeRequest === undefined) { - throw new Error('Required parameter authorizeRequest was null or undefined when calling toAspspGrantUsingPOST.'); - } - - let headers = this.defaultHeaders; - if (xRequestID !== undefined && xRequestID !== null) { - headers = headers.set('X-Request-ID', String(xRequestID)); - } - if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); - } - - // authentication (sessionCookie) required - if (this.configuration.apiKeys) { - const key: string | undefined = this.configuration.apiKeys["sessionCookie"] || this.configuration.apiKeys["sessionCookie"]; - if (key) { - } - } - - let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; - if (httpHeaderAcceptSelected === undefined) { - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); - } - if (httpHeaderAcceptSelected !== undefined) { - headers = headers.set('Accept', httpHeaderAcceptSelected); - } - - - // to determine the Content-Type header - const consumes: string[] = [ - 'application/json' - ]; - const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); - if (httpContentTypeSelected !== undefined) { - headers = headers.set('Content-Type', httpContentTypeSelected); - } - - let responseType: 'text' | 'json' = 'json'; - if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { - responseType = 'text'; - } - - return this.httpClient.post(`${this.configuration.basePath}/v1/consent/${encodeURIComponent(String(authId))}/toAspsp/grant`, - authorizeRequest, - { - responseType: responseType, - withCredentials: this.configuration.withCredentials, - headers: headers, - observe: observe, - reportProgress: reportProgress - } - ); - } - } diff --git a/consent-ui/src/app/api/model/authorizeRequest.ts b/consent-ui/src/app/api/model/authorizeRequest.ts deleted file mode 100644 index cea4b4d948..0000000000 --- a/consent-ui/src/app/api/model/authorizeRequest.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Open Banking Gateway - Consent Authorization API. - * Interface used by the PsuUserAgent to present consent authorization services to the PSU. The consent authorization process is triggered by redirecting the PSU from the [TppBankingApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#TppBankingApi) (2a) over the [FinTechApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#FinTechApi) (2b) to the /consent/{auth-id} entry point of this [ConsentAuthorisationApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#ConsentAuthorisationApi) (2c). The decision on whether the authorization process is embedded or redirected is taken by this ConsentAuthorisationApi. The following picture displays the overall architecture of this open banking consent authorisation api: ![High level architecture](/img/open-banking-consent-authorisation-api.png) #### User Agent This Api assumes that the PsuUserAgent is a modern browsers that : * automatically detects the \"302 Found\" response code and proceeds with the associated location url, * stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). This Api also assumes any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of amodern browser with respect to 30X and Cookies. #### Redirecting to the ConsentAuthorisationApi (2a) Any service request of the FinTechUI to the FinTechApi (1a) will be forwarded to the TppBankingApi (1b). This forward might contain a [PsuConsentSession](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#PsuConsentSession) that is used to identify the PSU in the world of the TPP. The TppBankingApi uses the provided PsuConsentSession to retrieve an eventualy suitable consent that will be used to forward the corresponding service request to the OpenBankingApi (1c) of the ASPSP. If there is no suitable consent, the TPP might still send a consent initiation request to the OpenBankingApi (1c). Whether this request is sent or not depends on the design of the target OpenBankingApi. Finally, the TppBankingApi will if necessary instruct the FinTechApi (2a) to redirect the PsuUgerAgent (2b) to the /consent/{auth-id} entry point of the ConsentAuthorisationApi (2c). #### Issolation Authorisation Request Processing The auth-id parameter is used to make sure paralell authorization requests are not mixup. #### SessionCookies and XSRF Each authorisation session started will be associated with a proper SessionCookie and a corresponding XSRF-TOKEN. * The request that sets a session cookie (E1) also add the X-XSRF-TOKEN to the response header. * The cookie path is always extended with the corresponding auth-id, so two Authorization processes can not share state. * Each authenticated request sent to the ConsentAuthorisationApi will provide the X-XSRF-TOKEN matching the sent SessionCookie. #### RedirectCookie and XSRF (R1) In a redirect approach (Redirecting PSU to the ASPSP), the The retruned AuthorizeResponse object contains information needed to present a suitable redirect info page to the PSU. Redirection can either be actively performed by the UIApplication or performed as a result of a 30x redirect response to the PsuUserAgent. In both cases, a RedirectCookie will be associated with the PsuUserAgent and a corresponding XSRF-TOKEN named redirectState will be addedto the back redirect url. #### Final Result of the Authorization Process The final result of the authorization process is a PsuCosentSession that is returned by the token endpoint of the TppBankingAPi to the FinTechApi (4c). This handle will (PsuCosentSession) will be stored by the FinTechApi and added a PSU identifying information to each service request associated with this PSU. - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -import { ConsentAuthRequirement } from './consentAuthRequirement'; - - -/** - * Contains information used to legitimate a request. - */ -export interface AuthorizeRequest { - consentAuth?: ConsentAuthRequirement; -} - diff --git a/consent-ui/src/app/api/model/consentAuth.ts b/consent-ui/src/app/api/model/consentAuth.ts index 0a229eaf82..a7a2f18f1f 100644 --- a/consent-ui/src/app/api/model/consentAuth.ts +++ b/consent-ui/src/app/api/model/consentAuth.ts @@ -27,6 +27,7 @@ export interface ConsentAuth { action?: ConsentAuth.ActionEnum; violations?: Array; accounts?: Array; + supportedConsentTypes?: Array; authMessageTemplate?: string; /** * An identification provided by the ASPSP for the later identification of the authentication method selection. @@ -53,6 +54,7 @@ export interface ConsentAuth { * List of sca methods for selection if necessary. */ scaMethods?: Array; + scaMethodSelected?: ScaUserData; scaStatus?: ScaStatus; singlePayment?: SinglePayment; challengeData?: ChallengeData; @@ -64,6 +66,12 @@ export namespace ConsentAuth { LISTTRANSACTIONS: 'LIST_TRANSACTIONS' as ActionEnum, INITIATEPAYMENT: 'INITIATE_PAYMENT' as ActionEnum }; + export type SupportedConsentTypesEnum = 'DEDICATED_ALL' | 'GLOBAL_ALL' | 'GLOBAL_ACCOUNTS'; + export const SupportedConsentTypesEnum = { + DEDICATEDALL: 'DEDICATED_ALL' as SupportedConsentTypesEnum, + GLOBALALL: 'GLOBAL_ALL' as SupportedConsentTypesEnum, + GLOBALACCOUNTS: 'GLOBAL_ACCOUNTS' as SupportedConsentTypesEnum + }; } diff --git a/consent-ui/src/app/api/model/consentAuthRequiredField.ts b/consent-ui/src/app/api/model/consentAuthRequiredField.ts deleted file mode 100644 index 27f1a9fa59..0000000000 --- a/consent-ui/src/app/api/model/consentAuthRequiredField.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Open Banking Gateway - Consent Authorization API. - * Interface used by the PsuUserAgent to present consent authorization services to the PSU. The consent authorization process is triggered by redirecting the PSU from the [TppBankingApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#TppBankingApi) (2a) over the [FinTechApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#FinTechApi) (2b) to the /consent/{auth-id} entry point of this [ConsentAuthorisationApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#ConsentAuthorisationApi) (2c). The decision on whether the authorization process is embedded or redirected is taken by this ConsentAuthorisationApi. The following picture displays the overall architecture of this open banking consent authorisation api: ![High level architecture](/img/open-banking-consent-authorisation-api.png) #### User Agent This Api assumes that the PsuUserAgent is a modern browsers that : * automatically detects the \"302 Found\" response code and proceeds with the associated location url, * stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). This Api also assumes any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of amodern browser with respect to 30X and Cookies. #### Redirecting to the ConsentAuthorisationApi (2a) Any service request of the FinTechUI to the FinTechApi (1a) will be forwarded to the TppBankingApi (1b). This forward might contain a [PsuConsentSession](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#PsuConsentSession) that is used to identify the PSU in the world of the TPP. The TppBankingApi uses the provided PsuConsentSession to retrieve an eventualy suitable consent that will be used to forward the corresponding service request to the OpenBankingApi (1c) of the ASPSP. If there is no suitable consent, the TPP might still send a consent initiation request to the OpenBankingApi (1c). Whether this request is sent or not depends on the design of the target OpenBankingApi. Finally, the TppBankingApi will if necessary instruct the FinTechApi (2a) to redirect the PsuUgerAgent (2b) to the /consent/{auth-id} entry point of the ConsentAuthorisationApi (2c). #### Issolation Authorisation Request Processing The auth-id parameter is used to make sure paralell authorization requests are not mixup. #### SessionCookies and XSRF Each authorisation session started will be associated with a proper SessionCookie and a corresponding XSRF-TOKEN. * The request that sets a session cookie (E1) also add the X-XSRF-TOKEN to the response header. * The cookie path is always extended with the corresponding auth-id, so two Authorization processes can not share state. * Each authenticated request sent to the ConsentAuthorisationApi will provide the X-XSRF-TOKEN matching the sent SessionCookie. #### RedirectCookie and XSRF (R1) In a redirect approach (Redirecting PSU to the ASPSP), the The retruned AuthorizeResponse object contains information needed to present a suitable redirect info page to the PSU. Redirection can either be actively performed by the UIApplication or performed as a result of a 30x redirect response to the PsuUserAgent. In both cases, a RedirectCookie will be associated with the PsuUserAgent and a corresponding XSRF-TOKEN named redirectState will be addedto the back redirect url. #### Final Result of the Authorization Process The final result of the authorization process is a PsuCosentSession that is returned by the token endpoint of the TppBankingAPi to the FinTechApi (4c). This handle will (PsuCosentSession) will be stored by the FinTechApi and added a PSU identifying information to each service request associated with this PSU. - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -/** - * Fields that are required to be filled in order to authorize consent - */ -export interface ConsentAuthRequiredField { - /** - * Field data type - boolean, string, etc. - */ - type?: string; - /** - * Scope of the field. - */ - scope?: string; - /** - * Field code - what does the field mean. - */ - code?: string; - /** - * Custom message that describes field meaning - */ - captionMessage?: string; -} - diff --git a/consent-ui/src/app/api/model/consentAuthRequirement.ts b/consent-ui/src/app/api/model/consentAuthRequirement.ts deleted file mode 100644 index f65b422233..0000000000 --- a/consent-ui/src/app/api/model/consentAuthRequirement.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Open Banking Gateway - Consent Authorization API. - * Interface used by the PsuUserAgent to present consent authorization services to the PSU. The consent authorization process is triggered by redirecting the PSU from the [TppBankingApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#TppBankingApi) (2a) over the [FinTechApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#FinTechApi) (2b) to the /consent/{auth-id} entry point of this [ConsentAuthorisationApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#ConsentAuthorisationApi) (2c). The decision on whether the authorization process is embedded or redirected is taken by this ConsentAuthorisationApi. The following picture displays the overall architecture of this open banking consent authorisation api: ![High level architecture](/img/open-banking-consent-authorisation-api.png) #### User Agent This Api assumes that the PsuUserAgent is a modern browsers that : * automatically detects the \"302 Found\" response code and proceeds with the associated location url, * stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). This Api also assumes any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of amodern browser with respect to 30X and Cookies. #### Redirecting to the ConsentAuthorisationApi (2a) Any service request of the FinTechUI to the FinTechApi (1a) will be forwarded to the TppBankingApi (1b). This forward might contain a [PsuConsentSession](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#PsuConsentSession) that is used to identify the PSU in the world of the TPP. The TppBankingApi uses the provided PsuConsentSession to retrieve an eventualy suitable consent that will be used to forward the corresponding service request to the OpenBankingApi (1c) of the ASPSP. If there is no suitable consent, the TPP might still send a consent initiation request to the OpenBankingApi (1c). Whether this request is sent or not depends on the design of the target OpenBankingApi. Finally, the TppBankingApi will if necessary instruct the FinTechApi (2a) to redirect the PsuUgerAgent (2b) to the /consent/{auth-id} entry point of the ConsentAuthorisationApi (2c). #### Issolation Authorisation Request Processing The auth-id parameter is used to make sure paralell authorization requests are not mixup. #### SessionCookies and XSRF Each authorisation session started will be associated with a proper SessionCookie and a corresponding XSRF-TOKEN. * The request that sets a session cookie (E1) also add the X-XSRF-TOKEN to the response header. * The cookie path is always extended with the corresponding auth-id, so two Authorization processes can not share state. * Each authenticated request sent to the ConsentAuthorisationApi will provide the X-XSRF-TOKEN matching the sent SessionCookie. #### RedirectCookie and XSRF (R1) In a redirect approach (Redirecting PSU to the ASPSP), the The retruned AuthorizeResponse object contains information needed to present a suitable redirect info page to the PSU. Redirection can either be actively performed by the UIApplication or performed as a result of a 30x redirect response to the PsuUserAgent. In both cases, a RedirectCookie will be associated with the PsuUserAgent and a corresponding XSRF-TOKEN named redirectState will be addedto the back redirect url. #### Final Result of the Authorization Process The final result of the authorization process is a PsuCosentSession that is returned by the token endpoint of the TppBankingAPi to the FinTechApi (4c). This handle will (PsuCosentSession) will be stored by the FinTechApi and added a PSU identifying information to each service request associated with this PSU. - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -import { ConsentAuthRequiredField } from './consentAuthRequiredField'; - - -/** - * Transport object for consent API request response - */ -export interface ConsentAuthRequirement { - fields?: Array; -} - diff --git a/consent-ui/src/app/api/model/denyRedirectRequest.ts b/consent-ui/src/app/api/model/denyRedirectRequest.ts deleted file mode 100644 index 160228642c..0000000000 --- a/consent-ui/src/app/api/model/denyRedirectRequest.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Open Banking Gateway - Consent Authorization API. - * Interface used by the PsuUserAgent to present consent authorization services to the PSU. The consent authorization process is triggered by redirecting the PSU from the [TppBankingApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#TppBankingApi) (2a) over the [FinTechApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#FinTechApi) (2b) to the /consent/{auth-id} entry point of this [ConsentAuthorisationApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#ConsentAuthorisationApi) (2c). The decision on whether the authorization process is embedded or redirected is taken by this ConsentAuthorisationApi. The following picture displays the overall architecture of this open banking consent authorisation api: ![High level architecture](/img/open-banking-consent-authorisation-api.png) #### User Agent This Api assumes that the PsuUserAgent is a modern browsers that : * automatically detects the \"302 Found\" response code and proceeds with the associated location url, * stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). This Api also assumes any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of amodern browser with respect to 30X and Cookies. #### Redirecting to the ConsentAuthorisationApi (2a) Any service request of the FinTechUI to the FinTechApi (1a) will be forwarded to the TppBankingApi (1b). This forward might contain a [PsuConsentSession](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#PsuConsentSession) that is used to identify the PSU in the world of the TPP. The TppBankingApi uses the provided PsuConsentSession to retrieve an eventualy suitable consent that will be used to forward the corresponding service request to the OpenBankingApi (1c) of the ASPSP. If there is no suitable consent, the TPP might still send a consent initiation request to the OpenBankingApi (1c). Whether this request is sent or not depends on the design of the target OpenBankingApi. Finally, the TppBankingApi will if necessary instruct the FinTechApi (2a) to redirect the PsuUgerAgent (2b) to the /consent/{auth-id} entry point of the ConsentAuthorisationApi (2c). #### Issolation Authorisation Request Processing The auth-id parameter is used to make sure paralell authorization requests are not mixup. #### SessionCookies and XSRF Each authorisation session started will be associated with a proper SessionCookie and a corresponding XSRF-TOKEN. * The request that sets a session cookie (E1) also add the X-XSRF-TOKEN to the response header. * The cookie path is always extended with the corresponding auth-id, so two Authorization processes can not share state. * Each authenticated request sent to the ConsentAuthorisationApi will provide the X-XSRF-TOKEN matching the sent SessionCookie. #### RedirectCookie and XSRF (R1) In a redirect approach (Redirecting PSU to the ASPSP), the The retruned AuthorizeResponse object contains information needed to present a suitable redirect info page to the PSU. Redirection can either be actively performed by the UIApplication or performed as a result of a 30x redirect response to the PsuUserAgent. In both cases, a RedirectCookie will be associated with the PsuUserAgent and a corresponding XSRF-TOKEN named redirectState will be addedto the back redirect url. #### Final Result of the Authorization Process The final result of the authorization process is a PsuCosentSession that is returned by the token endpoint of the TppBankingAPi to the FinTechApi (4c). This handle will (PsuCosentSession) will be stored by the FinTechApi and added a PSU identifying information to each service request associated with this PSU. - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -import { ConsentAuth } from './consentAuth'; - - -/** - * Denies a redirect to ASPSP requested by the ConsentAuthorisationApi - */ -export interface DenyRedirectRequest { - consentAuth?: ConsentAuth; - /** - * Will indicate if PSU wants to be sent back to FinTechApi. - */ - backToFinTech?: boolean; - /** - * In case there is no redirect back to TPP desired, exit page can be specified by ConsentAuthorisationApi - */ - exitPage?: string; - /** - * Set to true if consent object shall be forgotten or frozen. - */ - forgetConsent?: boolean; -} - diff --git a/consent-ui/src/app/api/model/denyRequest.ts b/consent-ui/src/app/api/model/denyRequest.ts deleted file mode 100644 index 703a40a0a2..0000000000 --- a/consent-ui/src/app/api/model/denyRequest.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Open Banking Gateway - Consent Authorization API. - * Interface used by the PsuUserAgent to present consent authorization services to the PSU. The consent authorization process is triggered by redirecting the PSU from the [TppBankingApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#TppBankingApi) (2a) over the [FinTechApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#FinTechApi) (2b) to the /consent/{auth-id} entry point of this [ConsentAuthorisationApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#ConsentAuthorisationApi) (2c). The decision on whether the authorization process is embedded or redirected is taken by this ConsentAuthorisationApi. The following picture displays the overall architecture of this open banking consent authorisation api: ![High level architecture](/img/open-banking-consent-authorisation-api.png) #### User Agent This Api assumes that the PsuUserAgent is a modern browsers that : * automatically detects the \"302 Found\" response code and proceeds with the associated location url, * stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). This Api also assumes any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of amodern browser with respect to 30X and Cookies. #### Redirecting to the ConsentAuthorisationApi (2a) Any service request of the FinTechUI to the FinTechApi (1a) will be forwarded to the TppBankingApi (1b). This forward might contain a [PsuConsentSession](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#PsuConsentSession) that is used to identify the PSU in the world of the TPP. The TppBankingApi uses the provided PsuConsentSession to retrieve an eventualy suitable consent that will be used to forward the corresponding service request to the OpenBankingApi (1c) of the ASPSP. If there is no suitable consent, the TPP might still send a consent initiation request to the OpenBankingApi (1c). Whether this request is sent or not depends on the design of the target OpenBankingApi. Finally, the TppBankingApi will if necessary instruct the FinTechApi (2a) to redirect the PsuUgerAgent (2b) to the /consent/{auth-id} entry point of the ConsentAuthorisationApi (2c). #### Issolation Authorisation Request Processing The auth-id parameter is used to make sure paralell authorization requests are not mixup. #### SessionCookies and XSRF Each authorisation session started will be associated with a proper SessionCookie and a corresponding XSRF-TOKEN. * The request that sets a session cookie (E1) also add the X-XSRF-TOKEN to the response header. * The cookie path is always extended with the corresponding auth-id, so two Authorization processes can not share state. * Each authenticated request sent to the ConsentAuthorisationApi will provide the X-XSRF-TOKEN matching the sent SessionCookie. #### RedirectCookie and XSRF (R1) In a redirect approach (Redirecting PSU to the ASPSP), the The retruned AuthorizeResponse object contains information needed to present a suitable redirect info page to the PSU. Redirection can either be actively performed by the UIApplication or performed as a result of a 30x redirect response to the PsuUserAgent. In both cases, a RedirectCookie will be associated with the PsuUserAgent and a corresponding XSRF-TOKEN named redirectState will be addedto the back redirect url. #### Final Result of the Authorization Process The final result of the authorization process is a PsuCosentSession that is returned by the token endpoint of the TppBankingAPi to the FinTechApi (4c). This handle will (PsuCosentSession) will be stored by the FinTechApi and added a PSU identifying information to each service request associated with this PSU. - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -import { ConsentAuth } from './consentAuth'; - - -/** - * Consent authorization denied descriptor - */ -export interface DenyRequest { - consentAuth?: ConsentAuth; - /** - * Will indicate if PSU wants to be sent back to FinTechApi. - */ - backToFinTech?: boolean; - /** - * In case there is no redirect back to TPP desired, exit page can be specified by ConsentAuthorisationApi - */ - exitPage?: string; - /** - * Set to true if consent object shall be forgotten or frozen. - */ - forgetConsent?: boolean; -} - diff --git a/consent-ui/src/app/api/model/inlineResponse200.ts b/consent-ui/src/app/api/model/inlineResponse200.ts deleted file mode 100644 index 1a326dcbea..0000000000 --- a/consent-ui/src/app/api/model/inlineResponse200.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Open Banking Gateway - Consent Authorization API. - * Interface used by the PsuUserAgent to present consent authorization services to the PSU. The consent authorization process is triggered by redirecting the PSU from the [TppBankingApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#TppBankingApi) (2a) over the [FinTechApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#FinTechApi) (2b) to the /consent/{auth-id} entry point of this [ConsentAuthorisationApi](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#ConsentAuthorisationApi) (2c). The decision on whether the authorization process is embedded or redirected is taken by this ConsentAuthorisationApi. The following picture displays the overall architecture of this open banking consent authorisation api: ![High level architecture](/img/open-banking-consent-authorisation-api.png) #### User Agent This Api assumes that the PsuUserAgent is a modern browsers that : * automatically detects the \"302 Found\" response code and proceeds with the associated location url, * stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). This Api also assumes any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of amodern browser with respect to 30X and Cookies. #### Redirecting to the ConsentAuthorisationApi (2a) Any service request of the FinTechUI to the FinTechApi (1a) will be forwarded to the TppBankingApi (1b). This forward might contain a [PsuConsentSession](https://adorsys.github.io/open-banking-gateway/doc/latest/architecture/dictionary#PsuConsentSession) that is used to identify the PSU in the world of the TPP. The TppBankingApi uses the provided PsuConsentSession to retrieve an eventualy suitable consent that will be used to forward the corresponding service request to the OpenBankingApi (1c) of the ASPSP. If there is no suitable consent, the TPP might still send a consent initiation request to the OpenBankingApi (1c). Whether this request is sent or not depends on the design of the target OpenBankingApi. Finally, the TppBankingApi will if necessary instruct the FinTechApi (2a) to redirect the PsuUgerAgent (2b) to the /consent/{auth-id} entry point of the ConsentAuthorisationApi (2c). #### Issolation Authorisation Request Processing The auth-id parameter is used to make sure paralell authorization requests are not mixup. #### SessionCookies and XSRF Each authorisation session started will be associated with a proper SessionCookie and a corresponding XSRF-TOKEN. * The request that sets a session cookie (E1) also add the X-XSRF-TOKEN to the response header. * The cookie path is always extended with the corresponding auth-id, so two Authorization processes can not share state. * Each authenticated request sent to the ConsentAuthorisationApi will provide the X-XSRF-TOKEN matching the sent SessionCookie. #### RedirectCookie and XSRF (R1) In a redirect approach (Redirecting PSU to the ASPSP), the The retruned AuthorizeResponse object contains information needed to present a suitable redirect info page to the PSU. Redirection can either be actively performed by the UIApplication or performed as a result of a 30x redirect response to the PsuUserAgent. In both cases, a RedirectCookie will be associated with the PsuUserAgent and a corresponding XSRF-TOKEN named redirectState will be addedto the back redirect url. #### Final Result of the Authorization Process The final result of the authorization process is a PsuCosentSession that is returned by the token endpoint of the TppBankingAPi to the FinTechApi (4c). This handle will (PsuCosentSession) will be stored by the FinTechApi and added a PSU identifying information to each service request associated with this PSU. - * - * The version of the OpenAPI document: 1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -import { ConsentAuth } from './consentAuth'; - - -export interface InlineResponse200 { - consentAuth?: ConsentAuth; -} - diff --git a/consent-ui/src/app/api/model/models.ts b/consent-ui/src/app/api/model/models.ts index 6059635bb8..0ae600ff23 100644 --- a/consent-ui/src/app/api/model/models.ts +++ b/consent-ui/src/app/api/model/models.ts @@ -8,14 +8,10 @@ export * from './aisAccountAccessInfo'; export * from './aisConsentRequest'; export * from './amount'; export * from './authViolation'; -export * from './authorizeRequest'; export * from './balanceType'; export * from './bulkPayment'; export * from './challengeData'; export * from './consentAuth'; -export * from './consentAuthRequiredField'; -export * from './consentAuthRequirement'; -export * from './denyRequest'; export * from './paymentProduct'; export * from './paymentStatus'; export * from './periodicPayment'; diff --git a/consent-ui/src/app/api/model/scaUserData.ts b/consent-ui/src/app/api/model/scaUserData.ts index 267f2711f8..5e8518a7e5 100644 --- a/consent-ui/src/app/api/model/scaUserData.ts +++ b/consent-ui/src/app/api/model/scaUserData.ts @@ -15,6 +15,7 @@ export interface ScaUserData { decoupled?: boolean; id?: string; methodValue?: string; + explanation?: string; scaMethod?: ScaUserData.ScaMethodEnum; staticTan?: string; usesStaticTan?: boolean; diff --git a/consent-ui/src/app/auth/anonymous/anonymous.component.spec.ts b/consent-ui/src/app/auth/anonymous/anonymous.component.spec.ts index d070f3f46e..9c13563fb3 100644 --- a/consent-ui/src/app/auth/anonymous/anonymous.component.spec.ts +++ b/consent-ui/src/app/auth/anonymous/anonymous.component.spec.ts @@ -45,7 +45,7 @@ describe('AnonymousComponent', () => { }); it('should call login service', () => { - authServiceSpy = spyOn(authService, 'userLoginForAnonymousPayment').and.callThrough(); + authServiceSpy = spyOn(authService, 'userLoginForAnonymous').and.callThrough(); const authID = route.snapshot.parent.params.authId; const redirectCode = 'redirectCode654'; diff --git a/consent-ui/src/app/auth/anonymous/anonymous.component.ts b/consent-ui/src/app/auth/anonymous/anonymous.component.ts index 4d48d2e273..5a69ad81ca 100644 --- a/consent-ui/src/app/auth/anonymous/anonymous.component.ts +++ b/consent-ui/src/app/auth/anonymous/anonymous.component.ts @@ -38,7 +38,7 @@ export class AnonymousComponent implements OnInit { private doLoginAsAnonymous() { localStorage.setItem(ApiHeaders.COOKIE_TTL, '0'); - this.authService.userLoginForAnonymousPayment(this.authId, this.redirectCode).subscribe((res) => { + this.authService.userLoginForAnonymous(this.authId, this.redirectCode).subscribe((res) => { this.sessionService.setTTL(this.authId, res.headers.get(ApiHeaders.COOKIE_TTL)); window.location.href = res.headers.get(ApiHeaders.LOCATION); }); diff --git a/consent-ui/src/app/auth/login/login.component.spec.ts b/consent-ui/src/app/auth/login/login.component.spec.ts index 880867d4e9..d5ede135e7 100644 --- a/consent-ui/src/app/auth/login/login.component.spec.ts +++ b/consent-ui/src/app/auth/login/login.component.spec.ts @@ -59,7 +59,7 @@ describe('LoginComponent', () => { }); it('should be true if the form is invalid', () => { - authServiceSpy = spyOn(authService, 'userLoginForConsent').and.returnValue(of(response)); + authServiceSpy = spyOn(authService, 'userLogin').and.returnValue(of(response)); form.controls.login.setValue(usernameInput); form.controls.password.setValue(''); @@ -70,7 +70,7 @@ describe('LoginComponent', () => { }); it('should call login service', () => { - authServiceSpy = spyOn(authService, 'userLoginForConsent').and.callThrough(); + authServiceSpy = spyOn(authService, 'userLogin').and.callThrough(); const authID = route.snapshot.parent.params.authId; const redirectCode = 'redirectCode654'; @@ -86,7 +86,7 @@ describe('LoginComponent', () => { }); it('should be invalid if password is not set', () => { - authServiceSpy = spyOn(authService, 'userLoginForConsent').and.returnValue(of(response)); + authServiceSpy = spyOn(authService, 'userLogin').and.returnValue(of(response)); form.controls.login.setValue(usernameInput); form.controls.password.setValue(''); @@ -96,7 +96,7 @@ describe('LoginComponent', () => { expect(component.loginForm.invalid).toBe(true); }); it('should be invalid if username is not set', () => { - authServiceSpy = spyOn(authService, 'userLoginForConsent').and.returnValue(of(response)); + authServiceSpy = spyOn(authService, 'userLogin').and.returnValue(of(response)); form.controls.login.setValue(''); form.controls.password.setValue(passwordInput); diff --git a/consent-ui/src/app/auth/login/login.component.ts b/consent-ui/src/app/auth/login/login.component.ts index 95dc908b9b..d49e75a1df 100644 --- a/consent-ui/src/app/auth/login/login.component.ts +++ b/consent-ui/src/app/auth/login/login.component.ts @@ -47,7 +47,7 @@ export class LoginComponent implements OnInit { onSubmit() { localStorage.setItem(ApiHeaders.COOKIE_TTL, '0'); - this.authService.userLoginForConsent(this.authId, this.redirectCode, this.loginForm.value).subscribe((res) => { + this.authService.userLogin(this.authId, this.redirectCode, this.loginForm.value).subscribe((res) => { this.sessionService.setTTL(this.authId, res.headers.get(ApiHeaders.COOKIE_TTL)); window.location.href = res.headers.get(ApiHeaders.LOCATION); }); diff --git a/consent-ui/src/app/common/auth.service.ts b/consent-ui/src/app/common/auth.service.ts index f05dcef9f2..3c3f4faaa3 100644 --- a/consent-ui/src/app/common/auth.service.ts +++ b/consent-ui/src/app/common/auth.service.ts @@ -11,13 +11,13 @@ export class AuthService { constructor( private http: HttpClient, private psuAuthService: PsuAuthenticationService, - private psuAuthForConsentApproval: PsuAuthenticationAndConsentApprovalService, + private psuAuth: PsuAuthenticationAndConsentApprovalService, private sessionService: SessionService ) {} - public userLoginForConsent(authorizationId: string, redirectCode: string, credentials: PsuAuthBody) { + public userLogin(authorizationId: string, redirectCode: string, credentials: PsuAuthBody) { const xRequestID = uuid.v4(); - return this.psuAuthForConsentApproval.loginForApproval( + return this.psuAuth.loginForApproval( xRequestID, authorizationId, redirectCode, @@ -26,9 +26,9 @@ export class AuthService { ); } - public userLoginForAnonymousPayment(authorizationId: string, redirectCode: string) { + public userLoginForAnonymous(authorizationId: string, redirectCode: string) { const xRequestID = uuid.v4(); - return this.psuAuthForConsentApproval.loginForAnonymousPaymentApproval( + return this.psuAuth.loginForAnonymousApproval( xRequestID, authorizationId, redirectCode, @@ -36,21 +36,6 @@ export class AuthService { ); } - public userLoginForPayment(authorizationId: string, redirectCode: string, credentials: PsuAuthBody) { - const xRequestID = uuid.v4(); - return this.psuAuthForConsentApproval.loginForPaymentApproval( - xRequestID, - authorizationId, - redirectCode, - credentials, - 'response' - ); - } - - public userLogin(credentials: PsuAuthBody) { - const xRequestID = uuid.v4(); - return this.psuAuthService.login(xRequestID, credentials, 'response'); - } public userRegister(credentials: PsuAuthBody) { const xRequestID = uuid.v4(); return this.psuAuthService.registration(xRequestID, credentials, 'response'); diff --git a/consent-ui/src/app/common/enter-pin/enter-pin.component.ts b/consent-ui/src/app/common/enter-pin/enter-pin.component.ts index a05bbc2e23..2549fa8081 100644 --- a/consent-ui/src/app/common/enter-pin/enter-pin.component.ts +++ b/consent-ui/src/app/common/enter-pin/enter-pin.component.ts @@ -38,13 +38,12 @@ export class EnterPinComponent implements OnInit { .embeddedUsingPOST( this.authorizationSessionId, StubUtil.X_REQUEST_ID, // TODO: real values instead of stubs - StubUtil.X_XSRF_TOKEN, // TODO: real values instead of stubs this.redirectCode, { scaAuthenticationData: { PSU_PASSWORD: this.pinForm.get('pin').value } }, 'response' ) .subscribe((res) => { - this.sessionService.setRedirectCode(this.authorizationSessionId, res.headers.get(ApiHeaders.REDIRECT_CODE)); + this.sessionService.setRedirectCode(this.authorizationSessionId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); this.enteredPin.emit(res); }); } diff --git a/consent-ui/src/app/common/enter-tan/enter-tan.component.spec.ts b/consent-ui/src/app/common/enter-tan/enter-tan.component.spec.ts index 88e699822d..351ae6d6fb 100644 --- a/consent-ui/src/app/common/enter-tan/enter-tan.component.spec.ts +++ b/consent-ui/src/app/common/enter-tan/enter-tan.component.spec.ts @@ -8,7 +8,7 @@ import { EnterTanComponent } from './enter-tan.component'; import { StubUtilTests } from '../../ais/common/stub-util-tests'; import { SessionService } from '../session.service'; import { UpdateConsentAuthorizationService } from '../../api'; -import { ConsentAuthorizationService } from '../../api/api/consentAuthorization.service'; +import { AuthStateConsentAuthorizationService } from '../../api'; describe('EnterTanComponent', () => { let component: EnterTanComponent; @@ -34,7 +34,7 @@ describe('EnterTanComponent', () => { component = fixture.componentInstance; sessionService = TestBed.inject(SessionService); updateConsentAuthorizationService = TestBed.inject(UpdateConsentAuthorizationService); - consentAuthorizationService = TestBed.inject(ConsentAuthorizationService); + consentAuthorizationService = TestBed.inject(AuthStateConsentAuthorizationService); sessionServiceSpy = spyOn(sessionService, 'getRedirectCode').and.returnValue(StubUtilTests.REDIRECT_ID); updateConsentAuthorizationServiceSpy = spyOn( updateConsentAuthorizationService, diff --git a/consent-ui/src/app/common/enter-tan/enter-tan.component.ts b/consent-ui/src/app/common/enter-tan/enter-tan.component.ts index 674779859f..fee6a2a9c2 100644 --- a/consent-ui/src/app/common/enter-tan/enter-tan.component.ts +++ b/consent-ui/src/app/common/enter-tan/enter-tan.component.ts @@ -5,7 +5,7 @@ import { StubUtil } from '../utils/stub-util'; import { UpdateConsentAuthorizationService } from '../../api'; import { ApiHeaders } from '../../api/api.headers'; import { SessionService } from '../session.service'; -import { ConsentAuthorizationService } from '../../api/api/consentAuthorization.service'; +import { AuthStateConsentAuthorizationService } from '../../api/api/authStateConsentAuthorization.service'; @Component({ selector: 'consent-app-enter-tan', @@ -30,7 +30,7 @@ export class EnterTanComponent implements OnInit { constructor( private formBuilder: FormBuilder, private sessionService: SessionService, - private consentAuthorizationService: ConsentAuthorizationService, + private consentAuthorizationService: AuthStateConsentAuthorizationService, private updateConsentAuthorizationService: UpdateConsentAuthorizationService ) {} @@ -46,7 +46,7 @@ export class EnterTanComponent implements OnInit { .subscribe((response) => { this.sessionService.setRedirectCode( this.authorizationSessionId, - response.headers.get(ApiHeaders.REDIRECT_CODE) + response.headers.get(ApiHeaders.X_XSRF_TOKEN) ); const authStateResponseBody: any = response.body; @@ -70,14 +70,13 @@ export class EnterTanComponent implements OnInit { this.updateConsentAuthorizationService .embeddedUsingPOST( this.authorizationSessionId, - StubUtil.X_REQUEST_ID, // TODO: real values instead of stubs - StubUtil.X_XSRF_TOKEN, // TODO: real values instead of stubs + StubUtil.X_REQUEST_ID, // TODO: real values instead of stub this.sessionService.getRedirectCode(this.authorizationSessionId), { scaAuthenticationData: { SCA_CHALLENGE_DATA: this.reportScaResultForm.get('tan').value } }, 'response' ) .subscribe((res) => { - this.sessionService.setRedirectCode(this.authorizationSessionId, res.headers.get(ApiHeaders.REDIRECT_CODE)); + this.sessionService.setRedirectCode(this.authorizationSessionId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); this.enteredSca.emit(res); }); } diff --git a/consent-ui/src/app/common/select-sca/select-sca.component.ts b/consent-ui/src/app/common/select-sca/select-sca.component.ts index 5a89c94a50..57c30835a4 100644 --- a/consent-ui/src/app/common/select-sca/select-sca.component.ts +++ b/consent-ui/src/app/common/select-sca/select-sca.component.ts @@ -40,14 +40,13 @@ export class SelectScaComponent implements OnInit { this.updateConsentAuthorizationService .embeddedUsingPOST( this.authorizationSessionId, - StubUtil.X_REQUEST_ID, // TODO: real values instead of stubs - StubUtil.X_XSRF_TOKEN, // TODO: real values instead of stubs + StubUtil.X_REQUEST_ID, // TODO: real values instead of stub this.redirectCode, { scaAuthenticationData: { SCA_CHALLENGE_ID: this.selectedMethod.value } }, 'response' ) .subscribe((res) => { - this.sessionService.setRedirectCode(this.authorizationSessionId, res.headers.get(ApiHeaders.REDIRECT_CODE)); + this.sessionService.setRedirectCode(this.authorizationSessionId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); this.selectedValue.emit(res); }); } @@ -58,7 +57,7 @@ export class SelectScaComponent implements OnInit { .subscribe((consentAuth) => { this.sessionService.setRedirectCode( this.authorizationSessionId, - consentAuth.headers.get(ApiHeaders.REDIRECT_CODE) + consentAuth.headers.get(ApiHeaders.X_XSRF_TOKEN) ); this.redirectCode = this.sessionService.getRedirectCode(this.authorizationSessionId); this.scaMethods = consentAuth.body.scaMethods; diff --git a/consent-ui/src/app/common/session.service.ts b/consent-ui/src/app/common/session.service.ts index 1914d583bb..ba223f8c1e 100644 --- a/consent-ui/src/app/common/session.service.ts +++ b/consent-ui/src/app/common/session.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import {ConsentAuth} from '../api'; @Injectable({ providedIn: 'root' @@ -37,6 +38,18 @@ export class SessionService { sessionStorage.setItem(authorizationId + Session.FINTECH_NAME, fintechName); } + public getConsentTypesSupported(authorizationId: string): ConsentAuth.SupportedConsentTypesEnum[] { + const supportedTypes = sessionStorage.getItem(authorizationId + Session.CONSENT_TYPES_SUPPORTED); + if (!supportedTypes) { + return null; + } + return JSON.parse(supportedTypes); + } + + public setConsentTypesSupported(authorizationId: string, consentTypes: ConsentAuth.SupportedConsentTypesEnum[]) { + sessionStorage.setItem(authorizationId + Session.CONSENT_TYPES_SUPPORTED, !consentTypes ? null : JSON.stringify(consentTypes)); + } + public getBankName(authorizationId: string): string { return sessionStorage.getItem(authorizationId + Session.BANK_NAME); } @@ -108,6 +121,7 @@ enum Session { PAYMENT_OBJECT = ':PAYMENT_OBJECT', PAYMENT_STATE = ':PAYMENT_STATE', FINTECH_NAME = ':FINTECH_NAME', + CONSENT_TYPES_SUPPORTED = ':CONSENT_TYPES_SUPPORTED', BANK_NAME = ':BANK_NAME', XSRF_TOKEN = 'XSRF_TOKEN', COOKIE_TTL = 'Cookie-TTL', diff --git a/consent-ui/src/app/common/utils/stub-util.ts b/consent-ui/src/app/common/utils/stub-util.ts index 6be08df249..821d86c56f 100644 --- a/consent-ui/src/app/common/utils/stub-util.ts +++ b/consent-ui/src/app/common/utils/stub-util.ts @@ -2,6 +2,5 @@ export class StubUtil { public static FINTECH_NAME = 'Awesome FinTech'; public static ASPSP_NAME = 'adorsys Sandbox'; - public static X_XSRF_TOKEN = '12b34483-242a-428b-8295-2f4805bb0a30'; public static X_REQUEST_ID = 'a8b24683-242a-428b-8295-2f4805bb0a30'; } diff --git a/consent-ui/src/app/pis/consent-payment-access-selection/consent-payment-access-selection.component.ts b/consent-ui/src/app/pis/consent-payment-access-selection/consent-payment-access-selection.component.ts index ed464fd4c7..1092904e67 100644 --- a/consent-ui/src/app/pis/consent-payment-access-selection/consent-payment-access-selection.component.ts +++ b/consent-ui/src/app/pis/consent-payment-access-selection/consent-payment-access-selection.component.ts @@ -1,14 +1,14 @@ -import { AfterContentChecked, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; - -import { DenyRequest, UpdateConsentAuthorizationService } from '../../api'; -import { ApiHeaders } from '../../api/api.headers'; -import { SessionService } from '../../common/session.service'; -import { AuthConsentState } from '../../ais/common/dto/auth-state'; -import { StubUtil } from '../../common/utils/stub-util'; -import { PaymentUtil } from '../common/payment-util'; -import { PisPayment } from '../common/models/pis-payment.model'; +import {AfterContentChecked, ChangeDetectorRef, Component, Input, OnInit} from '@angular/core'; +import {FormBuilder, FormGroup} from '@angular/forms'; +import {ActivatedRoute, Router} from '@angular/router'; + +import {UpdateConsentAuthorizationService} from '../../api'; +import {ApiHeaders} from '../../api/api.headers'; +import {SessionService} from '../../common/session.service'; +import {AuthConsentState} from '../../ais/common/dto/auth-state'; +import {StubUtil} from '../../common/utils/stub-util'; +import {PaymentUtil} from '../common/payment-util'; +import {PisPayment} from '../common/models/pis-payment.model'; @Component({ selector: 'consent-app-payment-access-selection', @@ -70,8 +70,6 @@ export class ConsentPaymentAccessSelectionComponent implements OnInit, AfterCont .denyUsingPOST( this.authorizationId, StubUtil.X_REQUEST_ID, // TODO: real values instead of stubs - StubUtil.X_XSRF_TOKEN, // TODO: real values instead of stubs - {} as DenyRequest, 'response' ) .subscribe((res) => { diff --git a/consent-ui/src/app/pis/initiation/payment-initiate.component.ts b/consent-ui/src/app/pis/initiation/payment-initiate.component.ts index 1e6855aaa3..70c45780e4 100644 --- a/consent-ui/src/app/pis/initiation/payment-initiate.component.ts +++ b/consent-ui/src/app/pis/initiation/payment-initiate.component.ts @@ -49,7 +49,7 @@ export class PaymentInitiateComponent implements OnInit { this.authStateConsentAuthorizationService .authUsingGET(authorizationId, redirectCode, 'response') .subscribe((res) => { - this.sessionService.setRedirectCode(authorizationId, res.headers.get(ApiHeaders.REDIRECT_CODE)); + this.sessionService.setRedirectCode(authorizationId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); // setting bank and fintech names this.sessionService.setBankName(authorizationId, (res.body as ConsentAuth).bankName); diff --git a/consent-ui/src/app/pis/payments-consent-review/payments-consent-review.component.ts b/consent-ui/src/app/pis/payments-consent-review/payments-consent-review.component.ts index e6d95099d2..198b1baef9 100644 --- a/consent-ui/src/app/pis/payments-consent-review/payments-consent-review.component.ts +++ b/consent-ui/src/app/pis/payments-consent-review/payments-consent-review.component.ts @@ -56,14 +56,13 @@ export class PaymentsConsentReviewComponent implements OnInit { this.updateConsentAuthorizationService .embeddedUsingPOST( this.authorizationId, - StubUtil.X_XSRF_TOKEN, StubUtil.X_REQUEST_ID, this.sessionService.getRedirectCode(this.authorizationId), body, 'response' ) .subscribe((res) => { - this.sessionService.setRedirectCode(this.authorizationId, res.headers.get(ApiHeaders.REDIRECT_CODE)); + this.sessionService.setRedirectCode(this.authorizationId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); window.location.href = res.headers.get(ApiHeaders.LOCATION); }); } diff --git a/consent-ui/src/app/pis/pis-routing.module.ts b/consent-ui/src/app/pis/pis-routing.module.ts index d958ebe0b6..4370c3c9d2 100644 --- a/consent-ui/src/app/pis/pis-routing.module.ts +++ b/consent-ui/src/app/pis/pis-routing.module.ts @@ -9,6 +9,7 @@ import { ToAspspPageComponent } from './to-aspsp-page/to-aspsp-page.component'; import { ResultPageComponent } from './result-page/result-page.component'; import { EntryPagePaymentsComponent } from './entry-page-payments/entry-page-payments.component'; import { PaymentsConsentReviewComponent } from './payments-consent-review/payments-consent-review.component'; +import { WaitForDecoupled } from './wait-for-decoupled/wait-for-decoupled'; const routes: Routes = [ { @@ -19,6 +20,7 @@ const routes: Routes = [ { path: EnterTanPageComponent.ROUTE, component: EnterTanPageComponent }, { path: SelectScaPageComponent.ROUTE, component: SelectScaPageComponent }, { path: ToAspspPageComponent.ROUTE, component: ToAspspPageComponent }, + { path: WaitForDecoupled.ROUTE, component: WaitForDecoupled }, { path: ResultPageComponent.ROUTE, component: ResultPageComponent }, { path: EntryPagePaymentsComponent.ROUTE, diff --git a/consent-ui/src/app/pis/pis.module.ts b/consent-ui/src/app/pis/pis.module.ts index fd100c78e3..c91117180c 100644 --- a/consent-ui/src/app/pis/pis.module.ts +++ b/consent-ui/src/app/pis/pis.module.ts @@ -13,6 +13,7 @@ import { PaymentsConsentReviewComponent } from './payments-consent-review/paymen import { ConsentPaymentAccessSelectionComponent } from './consent-payment-access-selection/consent-payment-access-selection.component'; import { DynamicInputsComponent } from './dynamic-inputs/dynamic-inputs.component'; import { PaymentInfoComponent } from './payment-info/payment-info.component'; +import {WaitForDecoupled} from "./wait-for-decoupled/wait-for-decoupled"; @NgModule({ declarations: [ @@ -27,7 +28,8 @@ import { PaymentInfoComponent } from './payment-info/payment-info.component'; PaymentsConsentReviewComponent, ConsentPaymentAccessSelectionComponent, DynamicInputsComponent, - PaymentInfoComponent + PaymentInfoComponent, + WaitForDecoupled ], imports: [SharedModule, PisRoutingModule] }) diff --git a/consent-ui/src/app/pis/result-page/result-page.component.ts b/consent-ui/src/app/pis/result-page/result-page.component.ts index d5e1aade08..3ac041bf5f 100644 --- a/consent-ui/src/app/pis/result-page/result-page.component.ts +++ b/consent-ui/src/app/pis/result-page/result-page.component.ts @@ -5,7 +5,6 @@ import { Location } from '@angular/common'; import { SessionService } from '../../common/session.service'; import { AuthStateConsentAuthorizationService, - DenyRequest, SinglePayment, UpdateConsentAuthorizationService } from '../../api'; @@ -74,8 +73,6 @@ export class ResultPageComponent implements OnInit { .denyUsingPOST( this.authorizationId, StubUtil.X_REQUEST_ID, // TODO: real values instead of stubs - StubUtil.X_XSRF_TOKEN, // TODO: real values instead of stubs - {} as DenyRequest, 'response' ) .subscribe((res) => { @@ -94,7 +91,7 @@ export class ResultPageComponent implements OnInit { private loadRedirectUri(authId: string, redirectCode: string) { this.authStateConsentAuthorizationService.authUsingGET(authId, redirectCode, 'response').subscribe((res) => { console.log(res); - this.sessionService.setRedirectCode(authId, res.headers.get(ApiHeaders.REDIRECT_CODE)); + this.sessionService.setRedirectCode(authId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); this.redirectTo = res.headers.get(ApiHeaders.LOCATION); }); } diff --git a/consent-ui/src/app/pis/to-aspsp-page/to-aspsp-page.component.ts b/consent-ui/src/app/pis/to-aspsp-page/to-aspsp-page.component.ts index 3e953a9002..4fa7340a19 100644 --- a/consent-ui/src/app/pis/to-aspsp-page/to-aspsp-page.component.ts +++ b/consent-ui/src/app/pis/to-aspsp-page/to-aspsp-page.component.ts @@ -1,14 +1,14 @@ -import { Component, OnInit } from '@angular/core'; -import { StubUtil } from '../../common/utils/stub-util'; -import { Action } from '../../common/utils/action'; -import { Location } from '@angular/common'; -import { ActivatedRoute } from '@angular/router'; -import { SessionService } from '../../common/session.service'; -import { AuthStateConsentAuthorizationService, DenyRequest, UpdateConsentAuthorizationService } from '../../api'; -import { ApiHeaders } from '../../api/api.headers'; -import { PaymentUtil } from '../common/payment-util'; -import { PisPayment } from '../common/models/pis-payment.model'; -import { combineLatest } from 'rxjs'; +import {Component, OnInit} from '@angular/core'; +import {StubUtil} from '../../common/utils/stub-util'; +import {Action} from '../../common/utils/action'; +import {Location} from '@angular/common'; +import {ActivatedRoute} from '@angular/router'; +import {SessionService} from '../../common/session.service'; +import {AuthStateConsentAuthorizationService, UpdateConsentAuthorizationService} from '../../api'; +import {ApiHeaders} from '../../api/api.headers'; +import {PaymentUtil} from '../common/payment-util'; +import {PisPayment} from '../common/models/pis-payment.model'; +import {combineLatest} from 'rxjs'; @Component({ selector: 'consent-app-to-aspsp-page', @@ -56,8 +56,6 @@ export class ToAspspPageComponent implements OnInit { .denyUsingPOST( this.authorizationId, StubUtil.X_REQUEST_ID, // TODO: real values instead of stubs - StubUtil.X_XSRF_TOKEN, // TODO: real values instead of stubs - {} as DenyRequest, 'response' ) .subscribe((res) => { @@ -69,7 +67,7 @@ export class ToAspspPageComponent implements OnInit { this.authStateConsentAuthorizationService .authUsingGET(this.authorizationId, this.sessionService.getRedirectCode(this.authorizationId), 'response') .subscribe((res) => { - this.sessionService.setRedirectCode(this.authorizationId, res.headers.get(ApiHeaders.REDIRECT_CODE)); + this.sessionService.setRedirectCode(this.authorizationId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); this.redirectTo = res.headers.get(ApiHeaders.LOCATION); }); } diff --git a/consent-ui/src/app/pis/wait-for-decoupled/wait-for-decoupled.html b/consent-ui/src/app/pis/wait-for-decoupled/wait-for-decoupled.html new file mode 100644 index 0000000000..29201e5112 --- /dev/null +++ b/consent-ui/src/app/pis/wait-for-decoupled/wait-for-decoupled.html @@ -0,0 +1,10 @@ +
+
+
+

Confirm consent on your application

+

+ {{authResponse?.scaMethodSelected.explanation}} +

+
+
+
diff --git a/consent-ui/src/app/pis/wait-for-decoupled/wait-for-decoupled.scss b/consent-ui/src/app/pis/wait-for-decoupled/wait-for-decoupled.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/consent-ui/src/app/pis/wait-for-decoupled/wait-for-decoupled.ts b/consent-ui/src/app/pis/wait-for-decoupled/wait-for-decoupled.ts new file mode 100644 index 0000000000..e10ee3c429 --- /dev/null +++ b/consent-ui/src/app/pis/wait-for-decoupled/wait-for-decoupled.ts @@ -0,0 +1,64 @@ +import {Component, OnInit} from '@angular/core'; +import { + AuthStateConsentAuthorizationService, + ConsentAuth, + PsuAuthRequest, + UpdateConsentAuthorizationService +} from "../../api"; +import {of, Subject} from "rxjs"; +import {SessionService} from "../../common/session.service"; +import {ActivatedRoute} from "@angular/router"; +import {delay, repeatWhen, switchMap, takeUntil, tap} from "rxjs/operators"; +import {StubUtil} from "../../common/utils/stub-util"; +import {ApiHeaders} from "../../api/api.headers"; + +@Component({ + selector: 'wait-for-decoupled-redirection', + templateUrl: './wait-for-decoupled.html', + styleUrls: ['./wait-for-decoupled.scss'] +}) +export class WaitForDecoupled implements OnInit { + public static ROUTE = 'wait-sca-finalization'; + + private readonly POLLING_DELAY_MS = 3000; + + authResponse: ConsentAuth | undefined; + + private authId: string; + + private decoupledCompleted = new Subject(); + + constructor( + private consentAuthorizationService: UpdateConsentAuthorizationService, + private consentStatusService: AuthStateConsentAuthorizationService, + private sessionService: SessionService, + private activatedRoute: ActivatedRoute + ) { + const route = this.activatedRoute.snapshot; + this.authId = route.parent.params.authId; + this.sessionService.setRedirectCode(this.authId, route.queryParams.redirectCode); + } + + ngOnInit() { + of(true) + .pipe(switchMap(_ => this.consentAuthorizationService.embeddedUsingPOST( + this.authId, + StubUtil.X_REQUEST_ID, + this.sessionService.getRedirectCode(this.authId), + {} as PsuAuthRequest, + 'response' + ))) + .pipe(repeatWhen(completed => completed.pipe(delay(this.POLLING_DELAY_MS))), tap()) + .pipe(takeUntil(this.decoupledCompleted)) + .subscribe(res => { + if (res.headers.get(ApiHeaders.X_XSRF_TOKEN)) { + this.sessionService.setRedirectCode(this.authId, res.headers.get(ApiHeaders.X_XSRF_TOKEN)); + } + this.authResponse = res.body; + + if (res.status === 202) { + window.location.href = res.headers.get(ApiHeaders.LOCATION); + } + }); + } +} diff --git a/consent-ui/src/styles.scss b/consent-ui/src/styles.scss index f8c14de3f1..a6a47ff132 100644 --- a/consent-ui/src/styles.scss +++ b/consent-ui/src/styles.scss @@ -89,6 +89,21 @@ a { padding-left: 1rem; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; font-size: 1.125rem; + + } + .customDate { + width: 12.5rem; + padding-top: 18px; + margin-left: 1rem; + } + .customNumber { + width: 7rem; + padding-top: 18px; + margin-left: 1rem; + margin-right: 1rem; + } + .customStrong { + padding: 5px 0 0 0px; } } @@ -110,6 +125,10 @@ a { .form-check { padding-left: 0; + .checkboxLabel { + padding: 0 0 0 20.5px; + } + label { font-family: var(--fontFamilyMedium); color: #21242c; @@ -149,6 +168,11 @@ a { } } +.inputWrapper { + //margin-left: 3.375rem; + margin-left: 2.03124rem; +} + @media (max-width: 767px) { .bg-sm-light { background-color: $COLOR_LIGHT_GREY-10; diff --git a/docker-compose.yml b/docker-compose.yml index 6f5f828194..35d464d833 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: - FACADE_ENCRYPTION_KEYSETPATH=/keysetpath/example-keyset.json - FACADE_URLS_EMBEDDED-UI-BASE-URL=http://localhost:14200 - PROTOCOL_GATEWAY-BASE-URL=http://localhost:18085 - image: "adorsys/open-banking-gateway-open-banking-gateway:0.19.1" + image: "adorsys/open-banking-gateway-open-banking-gateway:0.30.0.1" ports: - "18085:8085" volumes: @@ -46,7 +46,7 @@ services: environment: - BACKEND_URL=http://fintech-server:8086 restart: on-failure - image: "adorsys/open-banking-gateway-fintech-ui:0.19.1" + image: "adorsys/open-banking-gateway-fintech-ui:0.30.0.1" ports: - "24200:4200" depends_on: @@ -61,7 +61,7 @@ services: - FINTECH-UI_HOST=http://localhost:24200 - image: "adorsys/open-banking-gateway-fintech-server:0.19.1" + image: "adorsys/open-banking-gateway-fintech-server:0.30.0.1" ports: - "18086:8086" depends_on: @@ -73,7 +73,7 @@ services: environment: - BACKEND_URL=http://open-banking-gateway:8085 restart: on-failure - image: "adorsys/open-banking-gateway-consent-ui:0.19.1" + image: "adorsys/open-banking-gateway-consent-ui:0.30.0.1" ports: - "14200:4200" depends_on: @@ -83,7 +83,7 @@ services: hbci-sandbox-server: restart: on-failure - image: "adorsys/open-banking-gateway-hbci-sandbox-server:0.19.1" + image: "adorsys/open-banking-gateway-hbci-sandbox-server:0.30.0.1" ports: - "18090:8090" networks: diff --git a/docs/img/big-picture.png b/docs/img/big-picture.png new file mode 100644 index 0000000000000000000000000000000000000000..c29f4b6e043817a0c9d84ae58cdfbe90ce0b96ab GIT binary patch literal 147788 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

Crl*@|n->Wk2=JN5>_JaKGIU?^3krGz z@&bql#6X#WCf?gZ5&cmPkFCfmq2X&9wukQeJ#}?_8X6kK1c@^s4WTU|;QMAvP%4gn zkt2baf~ER%g8NB;Hvx`S{&zmyxsvSZL`4jFH{qfhEGu+!eZdEPdFS1A`;)+Eq2lIi z$41;Q0}YR5&M0!9onXd>-d3*mD;%sutyT)X8{S0n(KcvSYmepLCW&#mLgpv zK$bOYg(flMy^TKzSOn-Q{?ICvS7!3@<3|XTzln`S5HSFjK!8H1>hW(Sd(Cm}|9ZGEoq(!y8;GZ3M@o*d)-7HekEE!(b! zNB#oZ5t_qNuh$b5Y{5!D21i7Iq}x+2lmH$bfO8qryGRc?;}s(4nunHLk5`)(+}-v1 zlAO}9UIhi4=}fm7Nr7R8SH}-+cL5PlL)lsB<^o1eVqP8^Sr;)O85|>Nq=R+p-N%nF zLhmH1_bn4#6}mm0SN9KM%mxr63tF;`C#t=E4;S$Pi34G9lfI^Zgh@3TAiDkKPAM^A z+FtBPz#8VpjrjPM&K!M7S0Di)EI_aleOs)dzx7m|0rY@AsC2zH<}`Z-=y%MZo+s9K zJ9~QY>-Z5~n#9foJ{&h_!A$=2$zKQ!EA0bKNk}(#U|3iqbWv9GpBi)9)^KLGslxnqOawS*vS{t^T<0w0B&?E}Oxq`xyNiWcsM zPwNbaurM7!1pRz`@K2GG|Il}wld@c-0dHIoP3tq!R`%Q51&%Nv_cA)!=HZbWpr7(b zlm;yPm*ClNYrk|yyY~nKTd`Crl{oe`DJ^wDbgUMqbQJVpCW#E|yYHTZ{^;?uB5u3}>fb(wDJEp|uY5|zIc62Y&A z!vUKbKRyy+@HaFyG=%NE3LkuWv1nW#8iaq?pNB>;0)Inb1_zdxizAJ9z!oG@DLVHQ z8t`DPz%qNt>1D_Ov;kzZrAGVXPu1m?J<+LhKLd-}fV5B_7-L8ZSAZcV%-~-G9fMJ$ zvIJdo;A>C-40)nrd#nza1|PW-Vq=5A$TkM{*~u`)>srPMRKA%Y4H%$bktvg~lL1&B zO3@ej_+p&NgzdG>RXi^*ax6+|MLVdTZyWt84c-3;OBTf&L&h2&09~kOuUyHOzUD-+ zCL2nS$oooWE}`sul!Tv*AgyPBCI~(h7Z6b8;!LVc8Ue~L zv>QuLQShALl+Hbq%^n7T2<}P&lH^cnc^R_XcVVU@v5FS3r4UBQ+m2}ydQN_B{C5je zpU4!XE$3;x%_+bLzPfl#EgBc`{(Ts<%BQu0G7eL6(Ff1~w1zQ7;YpAQSuN1XiY72mq-Xl zc$UyKhBl_rwE`K41J_rD3gW7sP{I!VSV9l8zu*NG6@BR9r~6MWfC!*y`FiYUM*Xu8 zZLW!g@Y}*6_6Wo^jZbIjfO*0X-CPK73L!6OpTBtV_FNW6UmrFiMfW2k7C&;hVH?M1 zwKZ71jk8gZc_KWu2YhSUPx04dIly5{Nl9tDJ^(+VEKcWM#`6uk;`TE*&SN}kFU#TH zz34`Askr?@N$?xe7N4y|Gk+K6=O3;%XjHzDxqHa@#@ZnFS3S%3!L%lKhm~k;AG_CQ zEG_FmFoP%p)P~HkF%ZgV;Tr&{q-JHw0X+!;g9z_)U%g5TzES|dE`eQ$@;6+mA!b9R zwpuyPXB9CCp;6Vq89G%r0dNY`+$UiWYxvM}NQ8tmxM&2?TArbyU zw}cU43S`iBR}05B_MrX!`}Y7;h~6vx*FrVLprl~2d@2Ux6jW7liex9#GjJ%b99;-M zMP|e0V>(bavFZqaX@KXUW@1XmU1})$XeyR&yJ99m;`zl?)E~x=*=&8Cy z&|uK7+Xy}PVCR5-ZptQ~nwx9Mj>`oiGs3O3so11LgcGP82i5w!61xNlguLo#lMjS9 zPzqnl*Fq=ij|Mu)!wq#Ht$_dnzBwV}FA0(qo3ZBSpFDZ;^u_TkLk#il!R!YzECRkk z7_sl(Wv*V@hc%H*EO(%;e>|slVe2?9@b2j^g~ECH`G0BFtl|uakqeBvg~dg`ejDJK z)<7em0g&OqIzSvTguJm|0stVS(;XOuh9IAT)(o})+tI+X@$DsrDwuA0-cD_9a-7d)Eg8MCd>sJ1y zyjE!0B`ni-Q=l%0L-yVDYs~e|?xMr=Ky`!jB&P`N1H%zN`=-3im12iQCgeH8UNVvL z=~vbRW{jGOioNfJ`^Il-=;`I^+k}iLNpcGKwGgDNt6#n_HB}d^L{E1UFnA+{Tk0ZE ze;XVBmOWcnc09Xd*Pa4`wrodhh97JOPW%0a^MqLBiez%1Qc}KTC*_S3w8z(4X}9N+`?fA0qBRB&Ex=s z%;GODE3Q;ymy1kFPQ!e(pP5V*s9=*I#%WFjAXDF}s;W+bt`Ow#4h{}Zz$&g(oC$Bg zVZSop@CX6s0YVRm@Nq<&OpJ_^&?bB`TtBM6^8z@mT6-*ilc5O?))BIY8(Z)a7z~D8 zK!6%tA_z*+YyHkFFK2;ofR|GXkO@;Q8tKt%#{-1#u6ZbD9-S=qC8lKK_+8^RQtn3P zp}fp} zc92FZ8$YgyyKS8UT3snZeVHXvBARsLw zWBD7D>qQwR}8 z3HS0+>EG7J;rILUZxGm~Te{BuO_Q4QLQ>NQ7=}U*IFl{{+l=4+!10~9lbR7DIF_%= zy)4PyWT<%jLI@f-+PqzYJnLrIA1AO+E-B#hp1l_=KX6=k77aMmj; zPa(`a4J=>?)#1goT0afT3nkD@dh(VELQiY0e-@n;hC_!klw;QlCC^`xnGrCLjWx!U zJFOD|Hm7df2s84>G-9`dGW#KC zP*+>a4-e`f!s>50$TLkwMz#ui-2iB>N`=VN!2p6EsfGHtgH5le_d_!1veQt+sR(FDjamapF& zXu&j+0vaMEtGIc=B8kjHklc!(D2ae2kk%qBBsIC}2x|+GsOT5Wh0);7YN8>PDIz(u zG!zjL;kmK#JgxQf=g*vH&*Fi(kS?PtUwe3ed(vr?CXUz2f<0sOp20hY2aAR1{cE?& z@9pJiIGvjNDD)`xM%2ZQ`S-=gbCp`4B6DF#1p*}lDO?W*zR6H>Q9(hJn>OD|K+8^p zQ;kAFOBv$o!{c@K^~vezuz=}4F*g^~A+aCgR&!xz8$i=3V4)GA3T(z%kthM`V0ShF zVfaFyKZMznva&*90>I#pABI4fMa`W;-Da!>P@$;90t1|KWNZd-a#2$5?h zrS8WMMtGrH@cS-$Zk%ug44^Fa0T5GOEj}2>f|<6my-i=rE1e3JQ(6d2OXcCiA0{QA zD%ZigotU2H+_VS18(2sVi5O(Q02_C=8YD0PyZF=Jnv+wSkgFC;6<^sY#Ze|4fyK1p zl0Od=n0%4rjR}U8s0OYcVyA%<_oD;iPe7K9HydyR<`lw31AEkQb&3!*0Y@7_Ji=xJ z$B_6Rr2YvM1Pz`Z9XPqd9Y+E;F#iOF@Fn10pgh4+L4&17;}Qz|1qf@`wzr3czHXff zYKuk)Ht;q`#1!@Bx;?iGIUblSFnOv0_?Lh@CZGtwmvRjCMWWTfNt79`RKt@jO#^5MVwo>6lX<}w4Vd4^ zx#=NYzp0evK0x$;rH3&;tuc;z+Zwy<0kRwy@4X+m(bs|nj)o6VrQ$Ba#N0qIHH?9$ zh?=9LItG%(IVMshx22?Hy}<Hd<1a6Fi!Sgd3KN1raMG}7~Dh39U zD_5={SS29&17r~u-c$$pO#!2N62;i7q`=Kad;#b;^$j8L+T}NCraHxRl?31@D5&m3 z1Q|Rl7>_c)cYuxm1&ARigmNd~y7Lr4ih{%iSPYxCawM)(Lo5;}l`4kBsn*tD`scOD z>2S$Lh%Hi~(a6{^2=~-A>Xx@M<-k06lUvv!+FUvg|aVDrbDgRKDu{3*$TQi=s zDBQPSMCQEw^rOR>yF~b59r=~*<-PM0t|VVc#=VIP)I7@Y zog!5ruRE2)=oon*%QZdOT*o=+@j|kDt!L%a2D$Q$YYj}~F`Rtr^4?5=h%?+v4G%L% z7(5fVl1IuH5sZ^eZ94%xClYX{KL<-3oYpCZblUX5^+_ z*KS7p&bbwOov`v#jcM&z(e;H>LvCFZ>RZavO4j7pufHIet~XeTu4(3KmMY^LGF*K7 zwV2lSWko+O;8uk8yI{lYx=wrh`#PbeL2NdMB#kda2<&m^kpjK~ceu}~cS%mn+5Anp zU%2qOX0ZqT3fcv+#Rcbj$W@Rx2<0-?N&mD%N>OF_8%kpbnA;&1^k zuj-ya*8l02omt2HmqKDkA#r|X6HIFG-%`CZAUN?S|7Wxk4{}Zg2Vj9Rk#-|x0Er)k z&>DnL2r74nj&o9_^XyhWtaal{X$Mu|=Q_^^6UZyCNF9?O8Wwd>+>K1gZF0$(58a&Y z_p!Ao9!#Eiw7&SOnfrTf)w`nx_rkpmi;=)eJmdrIOzu%GJo+`%S*FO8#JX>)`q91p-+jPuoZ<%tCo3jRd8uuTjr_0&BWu zdeUYnU6{DI_d>GOsWCwYn;d6mJRIK6?ox-rJGl4sP|`@{dtFA}8?CbK<)OVm zZU|&nO(gC{;#O)sE>FquHD40{3i3RXBL#xR`>{(qH-5hONjuA4!G6ZFJ=0K;RvAL{n^PBgu^D9S&5>UkWyp`;*qSDuJrmYm+B}6{RP3Zut_e|})gM%Qh{@JG9 zE^M*$CO0@_0x*97g0s`rUii-rbxTZJ=}H#%kg01xFS|803uIXtu(DMjJZNUUbn#*h z=pDr0LWUXE)JU7pa{3!~!jv;Gksw9Gilj~Q^72qu$9REwh~%YQ-UAsLaOfjb%MTJD zDTdf=g7NEhl#(#$N+H|+1iUXX@^+9*CQfi&BSlqrccUR&D+*I40DH)17at@;6~m?U zHIz$f)Miaq?RyI>%ellQ{NuvG3j|3op_7pvGWfjqGkztU;=dWXWwu3hANZW*^pdsE zxy8y_vo;=`Ax;r#k`{+sfsuCz%N|&hA}Ds?5`y-JsXHMf?)SOtuw0ie-2^rz@UmV4 z5j?+1S4SuJ>7-0lbhLNVbPWV*fkbI6`G|oC(!LOAJ>w2>SeTk-`fAY#>_7Ox1I$o_S^nLj`-TDUnCVsXu~D7BYnmB_l5bHB64;@1Z^b43xJW)b;0rAUSR; zKpH`tF9`RBGoA0 zJm3)_J5K;W?WCd|B&xWlQJVG)(I(Ffw8pUz{6mIu075z!tboGrkk5gjA%U*0E`)^G zAs~-J28tki3*)%}ZHE)hT?cIS#-ASBgB@FNF1|F;18WxIq)B;TUSv9{jAN0TqofHO8LNWl>%5}5vKEqXWDwui{0I3RE;Gv>h z2XgYiIOK$b2jG5u_ySccUkiXaCKC)YU;%LANL&lX(ivJMp(EJsHAbGhx=e2&Y3}E7p%3;jQf1>uvE9gSg3~%o4~&Y zcN=El&@hRTLGWD;OvMkIWFfw=%z&k50ec&cz7Jf~CnLppu-XE~jk)R+P(g7L;G*>n z4ag)RsHu^K;xhz0+J!|dKgh$uAxEaVn3$QJK!NQ9ndJ~jr6E-d2$HbVX%pm8fX_ws zPX<7GS1MF+v}uft>zYG&q3Q8!@ZnitGgNCpQBiqK&4k~D0ntE;Plv9S*@&Y^^7di2G(UrbC) z{o3YyRPRzU0+NM^XDB8SwEMy8Emyd+IL@X19`DpdChr)3n7@Jka-tC9rvAz>=oW#8ujt>BLv3`>dp{`;RHmg z)JAQ9ij5R0!J+}&g5R?LGE!u0aRjA4EpxHv=4AuQ_A&=pW-1Soq#lm06QVlj1;v< zjk3Ey4yi3&+W=EsTufq042xrg;Nj~`&njYM40*Id@ zK|gWva%n3l9zxtZ6a<-7Q^`RA1w)n`nGpckYq+sK@$qMnbUeV!h2QvBL!IcbO%;s( zAV=oWL5e@upymXNm_rE9I;5*(4wt4{g8MIKkII4Lj_@W?-{g^M52R*7-{TNa*V{|F ze{-|S2>KbaZGTSzgdDBthy&`ppp;?>%oCJqk#`d;F9GsNe z&d!KXYe@Nn*HNOe9akOou-s7q2Du=C8C1j|A{!8jyNkQJ0OW%6ACjP;;~cRs0Yr%i z1gk?3RWO=}2^p^imj?0Xks_qd7kSX40LB8|9mH6mO8HOe&S@wy6h-C-7N9iLA@&6y z5wBVjqAj>Y9Z(54Y~G#G5MPMLi#({B03i+wR|k2wRRCzB?D3f_E$7V+vomclu%l5I`{xSiPW2U!5V@tf#H93m46PG zJ&wtEV=ifo!9W#=x1yMots_3^ditv~V*nRhL(vYkGcf=5oBMfwG{m6J7Q9ji;Rvd* zGw~Qk+`!O`8Ys1al0pgqX~KY+iJrTX2u$528-_rXmzQIrbM!xdbfA~h*G~}A!!VKp zwOATdEChQ4kR+q8Z*@(LSJusl6qSUX9Y>g_kShtnyAJ>^wc0<~y?t&}1hoQlg{I-n z7=*1DEK#J$?FF@M(sm~CiI&Xc$7#LW+K^g7?g9YEfdonkz889*pNlKdnh!6u!3r{L z5Z@{U^8aPHVtZSQm#$i$zi@$AYP2;K%%7Uk(KA5OM+zl<98(}x?!D*B-nlj_rP-kh zF%^J`LP;}%8GsW3!XUY@kPdDq*X7Hy&z=cF%-#3hBel$6|h1o1ft@J&HjW@n|25}A9u z;wbJgZwi452i{5yXHuF2?lTVI=p!{X5Oag-03nzM<`350(-IvRq{V>Bb=!AvSYZLt z3MuUgsNEBF-Qfq-agsH>N%;Z{cZhsY=^h_Bq#Fg|GB<5kDGRp(Lfa5Vdm)+#R6j(N zYi8Fcr=~crIuakf9eTPvc4JHI)=^8}Nk#RUsTZmn_X$uvV0%9yO_5HwK3M^%5suDl-cdUm-I;U^>VSBy$Ee zwjg+D3^V5zmI#xZO9zu-P!JHqyWt?Zv+ry27D8^uXr$3&2{bDg$EVRLP)R;EV4|P0tUasAn7iS z%)*8s_RE?1fgcab2AUcg{H*go{mnKaY78V3LgxnygNTBKgqS#h$c>hbjS@;dz{?;; zrZ6JI8lZBr&~4tvo@c2NZ!V7;hNy-^Y2KHnIle-!Y-`Wb%XFZ1kYZ#=-?R<;-(K2mY-&PAnmo&i@f`df z`3Y>hn=ozbG)(IaJ*NK(Hh$Rr|az2Xy(E zdC%J4wc4@6e<^E0xK_TD)~QG!$4CnCY#%^y!$1U(tuMnR_ORKTG_gMU`F1{ve$ntG zMsP53Z=`z};2>UWXlOttkRTQ**x^WMv!)~2_Ja87j%hV2X69g|BDJOl4-U!#>&HAP z$oQcylrcb4*MteZ$h@nPOBmQ4a_%QKO^odgYGRb@HSULo;= zK@#jB~x=A0XZb zHM!7rW1|}Hi`43oV#TCN{y$|KU)gRr`-0;stn~>w27%q(v+PjRwsZu&l$d60s?Wa* za*J~k>>@~qHGuK43Ug>+G%)N3GnC$fM^FouKro1h7;Hw!>gVCBfn>O>SI*Gb*oe^m zi&&q-#5^!Kkp+QyUno=r{^k)M{rH8#H z3iX%B3{1#}L4XB)<#p^fZ!}*`3NhLrd$6TzMd?Bhy!q!Rbf=4mde714obz>dev>fq5m#si{~i z-DCl07Kwx;nN=%7N!gp>5wY1EEn zJl@tb&3>?+C&D+%2*8tjAmOOoVIILsdKUNvPq`aOKl()$1Lr1xtsyj042C7_K$UNN z(^&A&twD_9tz0Kdh32FKIr?=sH@7O_D`Y6_-m79+H6>_>F?`LdFP-Oz|iJ4SkXOKhrN&VNPH?fvIrgCI>qCU?S4{|AHH z-hRQ4+X-s`!(6hQSKq|Kvkk&T5CLT`iL<(YGjy!YlyIt(Sw!HWTl&U?Kf|mwIz~pF zLTRAMI3jEev+ZavzEQRC654JWbue=ynwQ!W);m5@jUCMXa#Ff;E2{Z^v)G4(Tnhmo z<#E^N8B&*GqHo7UW_&B?dLO9xO6J3!@iZIVr#JnqN(8U=^X|~xA!VCz{jR?4v70AE z!BnHAAa$moa9N9$j4I}7cDD7>V#B9TCq8iVFjHWk>W}iu3g`xCCom607=#^47@3N!4YI>C1~S&7Y#N`@8rXRtSC@%Az4foP z^PDDVZpBzw*!a;enfaBeGgt*Q&}md^KJC@G6tu!v#v=Z;EXyappv!%}kuO>|_r&#e zZ(C}Yt@LDKU|H*DWS&`m>)E#Eu3~x}{bx<=bLG{N{61 zfs4Dez+l?y&=x8+WFmX&7msO?w^6TmeanOQS^)_Of)vYjmQWY{6556WXV>}hQp}lI z7P^g!sjguJGQ$37XlNjnCV&G1P!Mhs;6@Y(gl&tFl(djkxYt&Yfp!?a@+U&!oI8O5 zJ7ga{qBDKzo_o7lvu)fbqRGTb+hi?VJsInq1Mbtq&ZLki^ltv3c!BCKHE-9}ey^>y`;L$2*gSLIdebx04z%B;;x9JeKT`O$&#Y3cN+eBa1y{F})8P9p zXPnz%v!(HhniDEf67)4&TtUmQijsRR;?k{cC+34j zwfi2X75cUW)Hi4o1}YBS)l{m;NsK#Ki`s)0tg-2ytNKq-?z;2(Z(rwM({0&V9!vl+ zG|$10Sq`icl?>?^;AfljoY$eo3?D9@TV*QE?fU&K0VpuE@W|C>Fo2$`lK;qmq9s!6*y0l z`bE&7G*ZquImteG^5lMMCXr(OlL&vzSlQ2jPeLkO`oR+<6 zsja=2jqmh{^CJ82?g;x3=N(nt;_W>9;WPEmtBbm`vrF#_-{k!2b$dDv{8)FJb^B%3 zKA+Ir$!iN67NxNB-rik0JeSM1AUV6#tT_DiVaB#7K}`<&llt6D``I|fX}T(lrcz%> zx;3;E*qYUM{nAe}GBK_yu;rA>Zak){n!3I8aQ$^*VIiH6S6oNYK-p|e!z?N+>R*`9 zcP`<2>n+z0uXlu<+r49I?US!h_xJo_3NK(b;$+s?K?-#h6~AX`!X5FL^bj zgkusZqV}#xw?J$*>Q>I@eDf7!|H$$6x+7XUF@@(! zD4qH`^`X&zjq*#0d^~dJ={z<`iLcDso3-7gi%CrPZt1SY`6^c>-_gQaUgl(t^2>2P zPo71Uj~=S&+*+ykYeKSV$8#Zelj7EZ58HgBb_~BOo6+eTm0aS~)zu3+scz^slig&0 zMEs*jG{+b-l{r;Uor4*qvr=H8v1TjPRz+(YP(Hnw0beu4S&?J!n~jo^0mx%b2llXO zRQPEGxCq6l9FXhTK&2{BKR&eQo1YocFRb+qzI@n+Zum}{=h{&ov4>)wi(T+8KG@mw zE6A1m!(LG;%`ijx+g28jiXjnPp7RFh5z{W_4-f91$TZG>e}lf)PO91Mx%$N@{^;vh zL*54H=3PG9*T2~PKyb$fRT0%J{GGjb-@eqz$)4F)z*G24Z`E^BnU|}yY{kVN=}8Xz zbUhcSU{dVyUZdRs25v-w3;rNzarYlS6oIT8V?tZGw1Q@TwRE^PF*=y%m-S^_Zg%C^ zaa>!}z>wu!co06-G~b8bY_K5N?N%ds^aqbW62jH)@s3Ki>Wi{zDB$FhI$kN2bL6V_ zt4*(hhB?j0e>JdkUFVX`mAzO)!|Jd=)Bb>Zf3J@ z$?;cbWmNK-XS+c!O6A}e1-T;npLo+Cm~i5ZDwSs1NHn+qWyeLgkx z4ba<(MrjI4B$4C-7yzDS)JQ$BhXX?k?N3n>Z;z^p_y+{2#Rh1=AmW*ti0ZOepS~lB zL$feOG1py#k8m%-<`)TAU> zYUXrn9>xPQFi1HVO&l!n1zf7xuR+0p99=j0D?7yx4abTKS`g)~_3!L&7T2gC2`6b= zy13l`nj;6;2MWON4>Ox4z1gNknUD9`G%+}*glk7PdB)%YaBy&>a{TLQNbU#FN`%H3 z$4DTYJda@kWXuOlO(XFxmAq#;FjcfV<-ol<4B9fql-qtgg9NsT8@`J(#8Fp%S8iTj z{MTCrIm5Jj6A0lHqoRNb5?_X$J9&{7fh?%4tE+MR_+8zmI4(C%<}qFwJgWck+>-7> zFrMWxe4KP+&~_zJ91|2j5^dge=S8smZ@#rYgpZAdz6biRoWs_j1dB*YroM;2@NohN zqW=>CqlaWvZop+JwH`+{NJtP*2u~$}O4bZ`*6d=vVl%g0`O@8zSKVmb@$5Rk8RHMdva~6F zSRTd?XA$xe6Ad09q!3A*cFM&O^Y20_yZH zI)>jr``#PvYyV*FV;**1kU59?WTo2{1=Twm<|oPH4U7j2mZT2@ymB&FBY?(WqSBmyE-0maWM*D6ZOE95`Qyirq{)>-_#cN`n{rOc0OljT-tu5_lT$Gc8u4K1O)q7 zGvG;g@c;AE$U=q&TkW#Z{`y;6>_YG+og#jq4a)*pt2%V`IwuT>m$XI;=V9Z?%(FSFku{PG`Uf{9|T?}BpHA=C={TYT4?5w#1Jroq%~SgU700|S9BKO*pW|X zWV%54=}?5$T+BdYbiXJyV3@x9O`7@gT>@NO48S!A-hm<3yu7?b{{)7))PekGQQZG) zZ!cGAVE1+V#h&c5E(}WmJt@+DB`pO2idmKw3`Crol$2ytd+iZs*lL%eUP((&c#(VD zu~5j{$mvySD`t z-E~jiz?ev*Q#g!hK7X2ANn_Ygbc+eCapcd14VdnJX-8D=B zLKK{4s%`d@qnCreo;L327TgdwR-dIGI;&@DdfV`;NU`@E7qnfyivkL#bYlnSr0Ev| z1GydE^m=mw!-o}1QjKlF1SZ)Mxo$=M<3pO)kD_~n;c>^B^!#>)(fw8P2AGfRg~>An zng`B6J-6SUyU)=0+KyGJ6$8dHvib~}=6sXs$}l#e6P6)?{+#&YnAV@|5!$ktB}Up; z#BYO(E14W1?%pFWIYSO4m|vUuC$QIg*h46h;Zj&&B17rGI;djZJuh@vl)`p2<4MX@ z?dVg5KwC&PPN~fuK6~~o$GPJOR~m{AulC{jp&NjDlI`!F1VVOTO}8+bsu=^;64brb z{_-u-kF{-57Sz~2U4fB@W&~FmED1m;CW{WNTl2nv224PXlzV$eF?WA$f})2skCjn5 zGVZUrj+s~O8aZ)7KuvnUeM=|<3qrtbP)eNP-2(>fcaB*>;coQqtJUX}P+~E?fR6fRV+m{lyROZ?zx1KTe z(s!Ep$^?xCTr=ZTtHRz0Dk|E=V@0Ur5CPrX=D4bw2h9eV1{T@Bm@&8yp%_8&n0W6B z4K)Bm9bH{CK+6FW2eU8V=GgY+>Ok!+oI?R}45+oOje~7Uag3+vhn^2p;uh4!9(&ww zH$`rCUPakC))O*Vfrt z&Tq{3-t5#cYc&rfqKQ4cwNWafzgqJT{TN+F%it#`_VN0=R7x0~+UkEURR^q#OA$XB zDgh$1@$;JvOsQ7&iHnJezzC;bK9!vraO{~8g=wZp7P{SmGEJi*jv#iOyh~k+5IrH| zH=5a1rnIKTr}6MZyIOY5Tr-X?jR47dy`%X3m7N0vE2??mpewq0ZU{UZP^?(cD-@*k zu3#E&&Y~siMU=<4@7zJXSANIQ+M2$a2MRADd2lRQ;DVZt#)<{(mCpBkIN!u2G`_Vs z0gNLBD3JOet@+RET+{d1|rjShg)aYB$1U0D#y!GdeoT zBX@#se0*HircMaGZrVsWLXM%3nar})Y;0_$yB=X;rbVRlilxT0m;C%FgqwiRL0hI2 zavr$OL_%|v^i>|4D|KLhK=c|aGu;I9flNREzIo;v_a9(X*lq zDNh9wfc1Eu8eu`uCzU~)5h>SN;tLiB0BrmkXu-(y#98Fz;!=)9cK~4!@O+?L2;r84 zV{g-%msKy4RA2}Di9|#Zo3=vIjwFv8r+P|-pm1kp}VO^h*R=B!h5@{>J zzyUq`BLD!_UoWY1GbbYXnRZHsInWoe2JYW2jenMKYB%nVFtp-?RwN^|;^ZWc@z@KS zW$1$s{hox>pVo>6+!vaW{i2(Wyk&g z4GmTC@+vgV6hyIL^8Wg83&k)YR9goJg|3~n;l+Y*x@ENjGBn7ttI)L@99)Up4AWGD zpB6Ifo})!OH4VRh`}S@0-L`fL1q2BeEXuIjRQs4Mv1i1((aNIYd6{;cnXX&`3ouc$vCt8+~k+I_I*IPPw{ZmC$ z@qCcRaPDx`xw6yn;n;}?^hYo{meGm@^X&J4NeOy_SWYW`s|B=o7Lk_lv@^}eff2h; zwEpZRa6vE<*hP1CEahKZfXiQC7lNN{Ws#sV2Z;y)MffLq;}t`FMmP-65_ffXgA7)q zryms&L5$VVVJXkCf#&Gy`CGNMD0uEadi3Uz8mEB=r0~9lkT{1I(SHYLh2-E6CQ2K9 z;$>$)B$Fe=(KPwDr3AamOe707XxP_7w{na*n7jI`yGMz13DSza4dU79x1J%z3S9@^ z9N?A}Fs9ltJ`HjsT{TaE$8Z$NQt6n9-@nvK(X!f?;j8Z@FgT zct4r&Rs+$we1V=};~_jVt@`wp;N#nTOs40t3Jnh@`u8%P3b=)&UE@O{fbs?#mtV1M zF#Ne|-~Jd8a9a};A0=z2{Nz=c88k+gUj!8a2;v)Q{Gi&hgn!k+_32~BuN`V*;`+vN zj(V{BJvaFz)SaTzFSpufN>FWEV%1xeAKQPshGIf(EqdG7W6->)Juiq7-2Hxi2{t)8 zg2Dd&%eLKl2E<`=9zUVZx%0}uhr5+@Oq4^AS;p^hahuV*^?Ur&um^9l?C z(LN0ue#8ChVnKYEkcyeR{Yd@*ZEj{O0F*sRP||{46qEae)W}4{M)`TQ5&j}KxMmGG zXtI6}+%d0e%lO?@wIY&l+bW3hV{N4(T3!?-YqK@U&`3@~j1*S4>#f^?ZPqAx+4ev# zhUZbnqYfceC(c&*vpCr?6Soq|Q`mPod<(*)Y#ubtZ9d&+26nbmr4Tg)`WG&rC%wJA z@@h6l1OY;KEvba{79V=6h)ihVQ5%xvaoQ&q0hVJNaS*N~49~Iv`nvjhSUu3o%gY1X zpddiVR@Z?mPEJz{KvokW8`UBbBGMIvVTQKd&CP#_xMXP_Xqv`Jn(yulVrKjfS25mg z4lGtyR@h)&?`~~Fd9HO;Oz$bEXh&b(6iWTC3R8$f54VT_#m3Ga2uz0LhhI;DQblN{ z7|RD-f@qBAZZgFlc%dDA@|0fz$T`w4+-~|^mog#M6mC31TVIdx^LM_!93P$=>W3dAN3M19zMQP{=za~$KCfqay-9D zaUuk15R?QkSCw~Lg3`HXwHYh7n02EC4)LHL+Q%gSLC^>aE}D}r0eOh@nNj7b|)ttPU%EZ?QYW3dBEnY&wyT>_^Z+pZWNS;bUcCg`!mcCHYt zW5SS&g$2v*RgmIF^2i04>@eeaHso;dfM~s;mURpfdwO_yz~yO$REAh)!G;ycxP*7V zPuMMkgC$uQ(C#t`N!1w6KUb-1-_Gc-f2G51=v&6h)Ky`0y@x)_wEH=I+6ip~>>eo; zP?;rNVqNz7&MYpzpR%~iGMwOL9&+=|em4aw5*TEk2o1r&S@`f8)o{?GME?j`LyYxC zJ1s!9Vnq0O`sfwPm!A~A_x;4>QLJ_R_~;0e^t zUnAmzz;Y$J@2{Ws{Gx_s%CL$aU8l-H+`@3eXPy0d5VTXBo*h?m%s4nT12!*!Zb5?& z85%}H7Ew4=u=VewRLuu7sDSD6n2`NHM0V%8gzGpuTHM4t9%=o6)c+qJw1SFIh z{{SgUMn*q1c7FKVOGY%(BKZ}$#P{E3JGbg`%~hK_m+YHz{LjfhDJeYOHSmW0udl^t zzQ~ib5}izf4&_REX^Psvl#mRk`>X#jsMP3flQutcd+s?qEBpS3n+S-e%GQA3MyOfQ zC|px!bwdFdKF-VsO<_` zk|#^1)OSOI%nnc>4$Ms%d0!BW0ksG`?q2s{FM{|@P1ZcD5kU?)nwcuik|J@f!To?C z7jvrNpNEkWhExLy<&(xYkH88@4KW7caoH7yNTiU1e3(v;^)B9 z^Iw~(`H*%%>fPxyVp>(m>~$qb>NH&VjMN)6X(*ua`hVzaN@YR62;l6Dp|*d@K9Xb^ zrT)nk3~P`WclPuIfmr#5dl^me`mS$?z=INEYTxHP5o*q zCM}&&eaFAYExn++txW(PDuyi+%pd>zDhkxm3m|Fr4EG35C~)ZCTut+E5qr~J7G36u|GcqXh1!@tY*gvYGogPD`gFDbp^8<4iUqpiQ zl9-nVS|~`PXhq+HR-4d8Jc`rPr3eWIeG-uUW7%Vf`x!_Ekb6HxUoJ*O^`;$4eS(Ce zVOwo%?oE%VfTpcciU33rPR`Dqr$o;*KV60}j@En?dc2ftzFI4xAV-p42G-3eE?9B` zX-EKIOF&5xpq5O}%~jwz0D~r`ci8F6C}4sHfo4+RJ{WHO=DM>zlpfo1wVkk$(7`tw z%&w@Y0B^yDIK8lo>6XG>X{5o}T-C&ckCuVOAoZ=SdU`h-DrgQ@ol1d3LyV zen3~dC`6Xqx?}S?)h(o#_fHniZg2cHM*)Hw15$I|`wQWWs6!wSuxx&6iRnJ< zq;RH?d~vF63JRyLst9K&pHQD%-uN4i3?hidO>h|~2Wp(xSA2eAdHS(z5X3hq&;3$Z zyYnnlsbb7ikZF?mt+9SaCkIXNs!jm+G@U`FgJO{_75Woy_g~G+D0$F&hCQpCK(X~+XqA^>d?>-;pq}s_P~x< z%>W1}pj`r?>@vXVz2wY=aW5(6$r`|w#w8mKqpqkx@a^pF4MsJ*4t4{eu9PCz$a6)I zjERe5q(HOuU4T0@2nah-%CvT4*R%a=tr4xz_&msH#Hqln;A|PvbxI*SGrC$mJ)kVj zcp+gcefyRLY)NV%oFfIs2a!4al!f6cy#nvpq23YJ}kauv}b|cl-2oFNua1V$B zt}bn|1)YWI@3$Q5UV1nJ?qUbTWkeFh|7SKs(u0Xp>BGnJyn90~&R<+!l#v=myH_({ zCk~aAuv(E$umqWym?#vl$uFmgMFWMx%*@W2+C zy9u73ERqceZ@&jRam&}ItDHM1ASmP0bQAu8gvy2`OrhxO>yv9lT?PHr9tZ;PHfWIm z!Gn~BO^S+rWC7nO(&0jB4~k`)2=#u^b%_{ao06BGZ#mZUy48y2k839(RfEh65R`Bn zka5eNddoAE6A3wskv5enMFO)me_L8x2P0Plnqmoq1;RMIt8|VAaUtW9Rdr-%3qp<4 zA1gHCP=Y$gxk&)hHk`T)iKax-Mpenz*4~#Y7;!X>{cmJJ0>}d0fg3HwtUzqa2G$O? z2(2>G6KaT6UE)KM{yhYB!PtBb?P+4jdH)eZ>-nc0@~n5@b0NX-aR&z7<1m z+?v)g9UUE|{3cMAp!m3iveb2e5(+p5LY1_fp(~qmrV(X{LJhThAb??H!OJLEoELF! zKrj;sn83L&g326FkPT`emg>da+}u}dm_M27T+=2d(8%1x9sN8+RHMf|5W5FkvI8$) zW~GofhsY2NYZ<`PtJfUeqW@TeN)eKkWy2?t6I9Njb-P33>uv2`w4l}EWiv@hK>DMH z|ANf}7c6@_G!WL>^PB@sI9#w&rw1}iC=?(+>abcvK=^7q7iqBR?<7rSu$^&Y&|5=9 zwdiNbz1{KR>P#{kNliu#&MG2FlK7dwjy^xNGWS9$tPq2MR^YTHjybqNO^6eL zdl4r7+T;QBu=(x zhuiL$h>pPE13~9no-`4<0dyq#0aPH`?L2Cyv_(@`|C5Po9c32X9e5e@{D2&jo%Q*g z>uKa-zE_uw+>H!rsLV#_qOk>SBECuR^EYtc!bGp~pJc91m+9k?^jw3hf|8t~h&$>6mw-H|e=^>G zwb=p;Jg}~7{oqc!oAmDgq?7;dcna0?yM4(934)c>Js->jgOA6tT}ln-|9^cD@Zr>( zD%5aIqnc%5L8<@s8~^_U*m|(FK)0|gPhwtPk|P;OoA6S3`x=*nKjfT62nVC({VDRG z|9xZ3;pD~)ALa~`=Qpx*&&#cpWcDr2%=dSS+t!K*D=d3^HL0QHF!^PnK}|E6NmE$@ zPZ~{}yI(xhCJ05^#NoQRBdnRf%{QiJ)CLhQ1l<8bx9DeA8yp$ET>08hBB`dbh}CO; z-TEtj0e!5;eCdz(l^gDpJgGS&wDH*XEN*e?vt+Mm&fY_517Z8>CB>t!rECbnXUTQe z(P6!`T75W_5O_|$`OMSDE<;Vr%$xr5HdGZge8TwqW$mupaL#O3%_Q`@JzFweZot_F}F2y}K&sZK{}q@jN!xGRo~fCft8tU|)Nqa#rT; z6uo(Ok$CZnpg{I|`2&ZdM{Z7lks}Ta|bE= zmuyv`mvr3*hACHjYNb6_K6-baC#xcyeK`(b^AuLCUrz%*pW1bjJEdko4KFJco%E!7 z<$SLhdFi?1_Y(~~-!^-O>rCsNTC3TFgvb_uZEfvPCtCzd;XEHQW%$xiQ%o3 z>?18S`0DX^)5D~i&d(R~noKV|-WuY_&yW+E^iDio&hw4XQ2guV><^lZV&trvUX10) z%*rTjoNu~nSuH(N$lb5w^}?68Al>>$&h;|ud(00Vh65e!);zQyrj5HK)|z#BGOcOg zQ%Cohb+GRg7+IFs>INCwBSlO-j}93~C>?(!uI|_pCi$F6GNkPVyXT#|PsR&s*X?_L zg01)S6?TSXk=jS=@l}ueJE^!vT(N!r3$34-5e1d=Rtl-HJKtU{6Cd&WH1PP9=bhEG zMVwN>H9vg24>@Y;6uysczmTM?F`34*-QM;!Gwp%S7mwFJGk=Whsq20=toiQ?`d>i> zWR3bQIk#DS#8*`tPD9dI(tGmhrvZr%+9f3+R~eQL&kuhbThcF)X|KFX-O>a_bK zO=rpCk0I+v<2xnS1)VewUP?{t**X4|_pHeqi;{;*c}q+BqHAaMcA3Y8ti_J`hoFSE zJzDwwyyefStBgZKw(y~@eYY=q>I|K#LGr_$ojGHNHI(pC4V4)a^XAL-tF7IsnFmA?X62fq2bS|BK=NV1B4_F9J<4{ zZDW~z6XijZ-i@-?(gnQ?uek5V8jO}~=1a0<_;2Dyf(A|S&WkD8RNgLJN$Aa31LNnb z=~TmR_G@^^yp$ACH9L(rnfy!Swb5UPY0@v%bp-yK7=`*rU&LSSo4QSU5Q7qKDE9RF z`i6hq5WA;}w#eJvTh)KM9SmlBC4I%TS2#Yl?oy+c&By`1m=W}MJv}{%j~BSq6^W{W zPQ)tEN5_x_s4qIq5bhqm)E9ydwj4t>4^ry`19nIp{pCU+KGQKftViT%k#b0{3mdIt zuU&(2Z_wP=fA_8%t=T6)p7;~U$%7Z*JiUH%&ldn1+czd<;HCn|`*a^@aT4)Q7}5SY zaUdA0DiGg@?wOUrgD1~i@gyfRdMeP20hmTkMN)-)x(o37cUjCil1muyk#cAmKCE`# zX5Uho{T%sxcJX1ea3YzlsH&m|q24b2O@C{?0*@8R+(>aQvP7spkrZFAH>YKPF$_; z{OCoc66wYbpF)XCEZRtr8RMU52G;i0||7n&~T-B8=(-xt zfaUb*36Ik5CN1H=SA#9xUjF5KMdplHT643L@MM%uu4V?@WD%JJ;Q%RscII4|av(|M z?#1bK6d^!s#7!Q>JwO9=)B`Z2U}zpyJs@-N8Q94G3=3mM^EMcglMuy=;QGSImh|YM z&><>FqJCkVS7a^DnLPiTB++{XOm&E#cYYt;#A2}nEe!#NTodLXdMuwU$*+G z?~>2Z^wQ$gZv3}%x7JV=iU@S>A$5&uN|nIGy-r4$a4!QRetNq5az)XWk6o~@b z4;T&8F7Zv9bbHcQf(^*C&0#Hk)hu8~g^mo!@g;M`*@elMcPixpXc6Vl(=d_;%>rL&DwWsJm<#zKh@aUuj@!0 zGuL-xIv07Pc2x2#`*iVS=+uCD-$cnm`j~k|2*;?;LfO<{NMzl+eM=sP*q((3!HEF9 z*^Ul_?tHY^Ko)?Y8c2Ix%7JxIQ&T+Seqa%EfLU$&sRiGgNAr?G0j~E?Ll}kyD0Yd( z!KqVXC_yZ6_uwsI6PE(*j+FD7{rQL*y9DVL5MG){IkG=V_wOqgAOyS(D2vGel1Mq! zHqJG6ROaN2^=!<5GE~u4B`AlqWrE>)q3CjuA3CPb5n-iifvrUt0OaXl~L1~NZAY7tphT1o+51zJ=Mtew1PFsd}u z6b0s{3Q_q+A`UaXgFezZt9GeX;f)Y7anpf#j>vdd`{=20rf{oF z+SisG9B$OAtfrW$9@!J`bIQM7d%5}WhJLTNhEY9ySEJWFV{jCCFHvOA@vQ1rsDh{4 z)S>&+`o^;*39sVxL=h?&BIV%0UfA-RhK2?U7i~cF*6AqQU3!>!tVn0k&m+tS1_=2_ z>1Y{O^*mfE0ciB>@_*Xpr@2 z{7uPFg|3=18X+aH-O2A3eGFP!fGP3LE#SUD@ym5=FNbe;q#Pz#(WIQWQ@4)6uB(<* z;6P^rFgvQ3mFW5swm+oe)~ebe8#r?oPA3tw3B>RyXJ6) zA5{PA$-(+B@sHQ;_0`3|qO{nyRm*2Is_*DW_w3gExfE3^LZxGd4S`j5pZCH<6I#ME zW4*0bz^4$8fNxOR!iet9sShM0U5RDx9v*#EYjkP|28JdC1BG}U^<49==9ynuSTAW# zjUfP9H4n&~7~Zv2E!syW08rp$3(tS#b;s>cZ?-tu2j6xW?4F^U9 zRFp^?9H4hQZQie4GR0`2=Zi<)?W*S8{3%+puSM9Nc@IvuD*A|1L)fPmCU*~EwM|)+d z@uqQninUnt93aJfB1lUs%b?np@o4uFVdoY!VR`7WTiD3{y?%vu9urYV)mN z3o6u$=xrhTLNgqy_`Z8|yD-6KIISyS%@a)R0MV%fk0oB|oIGrJ@=cauu~DFFl|Bc+ zj6EDqh+(yH8lqk^y>TaktI%g6n!;^Pb7!V&2)GRO2CHFjCz%*d@%yOlTE;816%5 zo>+^&*%sfdMG;4{WF2K%yADCpynE&^_M0O6ldegGFYtfRzxDCRgOulThvbf594>pq zcj#Ih!}6K4={Iuz#Rbr+6}&pS(<}4f5`U~)QwS*E=!Nfa{Z0UCm6;P4P;w)5Kn)ZC zyL}$3r&(E@?^la{4Vas=+cys4Y!(iY+sfX&D*1+_SIJ}549{Y+tg*OoFDdeQVm1S8mf&xjpRR2nQCboz~$vyC0eczxxbbGbUlY`ovpCWIB$N2b4eR8}Y zS+vM8c4=HlKiIx(qcn0Iw0#4!vgH17<f!jqo=2*3n>&La1ijRT#If%e~EAcMid|hwXK5;03@lgtcADiK?K>f`J1^4 z%oBL5kot8uo@Wz+VF?x=>D%*cG=KFG=LrB&?%55_S~l=fh%mhyCS6#~bV5RU>-of;Yh&`k|? zA9$+1!rR*s!SG}mfX)d2gX<%&O)h3wf10BfE0N<9Qs(gR0}1HYpbL5e@D`SdKbovi z=Y%2CbA8V0H)fs-qX=~Z{z;kS1fdfGy;l`~oajeZ;L2!Czzi!CLVVb$`F}l5rNB`+ zTv$CM5&Z|^6%Fw)B+DC}XTY+CtQ<}7YA{XGUL1Ow*bVQG)U`0$;^wi6-~1QGKR_0Z zCO!->s^A}$PtQm%k~BsZ6NT8~kRfL{4>fTd_y4qjU_`?kBv0sA#z+h0kE;uPk z7($7Q*oDq&T2DiJA%C-g)EKFZ(z!8>p2^F|d5PmR;vsRiM~3Gdv4uvI$}94p>I0eq z7pmi?7E7xGgI~TnqYa<3&Ae{za*a^a7VH-T2G{N^5+R_ z*%Nwb=3}^jQ&(&y1c73_p8(QjySc;MNCEET9zLLi{}1)I^8^i{&{V#@CB~vtL4>dX4psvu4=Q5XNQDT`)+KB zTl)6;;hnc`gO=@Ke0#v1v$6Usq~c{`Q|iJ-?swJ2>1i4Qb5*BV*S>TMDZVTHOYy74 zw`VgFdR|BRyhUE0PR{!f7x=Z2Y_8UU^t5}IKC0h|nf}2$#XCFmYqg0qsoMG#$O|a^ z`s+VB6zSd=@~98o>e)0tbx2meuA=JROI)q^G;4;%rwmcMw+&n`wTpEc8)akJ2WJk4`nGS67(cx%+b6x6 z#noS@UBGFu=}_`-yRUy9WeSggLUq}NZ&!}Rtz%=R$v@2DE0~ZuO-bLRYiIOudyLaS zm*q84r4*w(?bY7`{e5({PyLM2OW0)Cd?Y0T!CLfA4BOO*@#ephh2shi>jvns3wBLC zZ%@R%htB@K^42<>oq2UxvFzr$ifo0s(XB%Q@^|UU-5klc$uVs?_=GwjG@KJ; z7<1FBnmaRE+vS9Pcz(6=DTw76pIfQmi-llKIL!DzkKSga z`3<@A$FO_zJqTmXa_*&h@k~~86nz9S?nb6T8 z+8DM;KA+ulD|Iiew8VEZOQx@--LuMlpXW0~ncdN{V&T)TA_wNJ8K$KRMXnWln)6&# zV_jA`m_(dAw8yIJmFm;Y+sd|2`FpND&#Aqt;GnsX*tsMY4n?l3r|~RTUGLos}`qD z&=popgbD zP1<#h#!Ib18Btri1QwS|rJqu(Ig|di<2R>ZDSnFURJCbhJ)C`EA|&%&UunfW^Ly_CtI7K@vRk%l zEFJ$w7M)POQj%Hj8D%uV>fR)+P`w*4!9%+jEf^57$0G@sF*Q4Z=Y|0EYJQ4~y!*@T4zyQ- zPm3~XsOg3ZvW%N_b!H2WATTKP?XUi|cU}I@hzpmn7jXL>KFe-zcVU}6?_JLSJllab zu6WnTP7yXNGMlCrnv%tl>|^0{?CWUfFU*NxSz3)7oPKgli976a2J!}drPKE$7L^yL zM}^M1y@;9Z(c+9Tc*R=Qw^6w?MVL14zL8Gan!9Z~FRS@5ag}Tf)|s!IxS=A-QPo~E z;=k~8@!nMa&chria({g-SZ=*Hi5xnnvy+r76;A!PWI7OgU6LKUU%&fZ*YJ^xcg1t@ zOO+mqP4}j&ym|QgUFDw?xTgkcqe_};mL|WGJ8idy%th&js1>3s#LKW?oCe?e#Pjlr zD5iQT9m&qB3@f^IV7^OPoIZN(Vf!U!C|-YGtjmerf#^?pZK=&Pw-Sd}hn&^g0BQcR zuXz{L?2jycoQsz&UrAr6=;GrW9zqB+Sk1zNhfGF+qcS23v4XWRLgv@>b<~N^{wWY zt|bLB(Z8 z*YEqWcX++gipB?HWDy#N3K2xA$EF^w`!)Ml^O0<>^D7I^KGk{^%kKHhcNN2|RQ_D- zMzP~2htHj;6nZ52`HpL{qbsvbYlS&E7(INmC1OTO-fhgXXrvpOd93e{c=Y*037zHX zlUvkWco-C?s&OFs_sHH}a!{O|e9;~Ctza0VxPX>gpsetF(_5U*Pj##V1f+_m;HyjF+*zotNpPHl02i zqiXXw?Na_Bt%}r3>uay5<-1CT2R+o=lD~6-{p_;IJNAA(V;jD6UmohZt8n3yw!`hd z-$UrZU1kqL%qh>#|Fectrtgv+h#M~Rb<+Hmr}Aus7hub^%pwx+hgvK5G?CX#)5d>W z`6=i3hgvEwN3s`kT|S;E+-A>$weRebU>sZEi|aa@lO5n5@jZTvGo3hZ%aNO%Lo)5I zeK_XcGP62QH0Qc;ibXng-&MdFWV$0iZf@>-@NntNKJiI~10x=8e~xHPw6~KUG_YVX z*1dQvhUfVG)aF0=pfHYRf_^!wH}*X7^I!zj!L?&uCvrcMAt1fu%#{)B6)5qY5S_C2 z>~BtL%^bi%_?CHSWY^)lt)@GR`Ml3Jt72E;A}S*4(wGX)(!I;RSyue~lEHk}b4DZx zy*^c%EkK%E_D-_qGC?n4_Th`eB8WN-I((x9nS-+es*|>a@FX73${t+<;Ho zmgu#mxtVc%bljc$9kyf{Z)cv(!OY1Wet~~AlioWqZLhN{#k3gI`g{tVxC)!39?P#w z+O&Z~H7-g8v2-2_qe^DTs-Y{NWSvMqWSjGIqqW%@P6x=J*5?gtynae8%uF-Jz&WP$ zm4*3GQBy@RyIRKWj;QpbvjW@R`|d2HMatSbr;gtyy&)sxPQMUUGIQb6w~Tc|)BN0u z(sY?_RifMFo4Hq+DR#UX`am{8^UF0PNyxamQtYw-ii66D_FB#7Nl0F+hV@htxYR@* zpCI=({9!FC@(Z%!fbQM1tC>_wNC@!ZHooj_V$AH_h+U-WS$f$hWBriA>epL*i$e>t zlTK_t8AI^gv;Fmdra6p{YhYSoy*0RzNnnzbvI$;MG4b&dZiL2EMJ5Yjw&)9oZ@lf^ zQ+MjEUtI-=Obqs*0n>nX!p;mP!UDzfV>($iVCiEp$Yn?jFuE|UvFQ*!TyhC{9YalZ z`ul(s@?%C}?Cd$zcVym4{f9%=i;HuPw|DyrP)0uP5ft9F%WSCWADA!~`S+7Jb?%`Y zT2;Jd6)bmbJ9?{&FP9I-cU(Bb9m4vHQ8X?h=y$t)UlhXhK&Z-4TQ?54ctz{YSaa$c zPc!QlTClRseQ&GoAGcrIn!|l{Y860BLiTa}xX1pAO?H&#`U=+aY? zS>ZZ#(i5Bbo6b_%+tDrWwU2DSkZfSS**-PwQ{}~mVlAXAT~~}%bp>|ODJq-4U(Z^S zun+=QrUA*fcjS*rX_8zfzeI2@fh_rvh-eV5*rqL(SKsxdKz&`W5Yirk7pY z9RiW@zM6KFia;tPEK1dwIC}Ub8#1{>oV6Y@=SCyuRGvN6{&B{xYFF+Fk^Z5^gsbj& z7OflZEjd?p^}kj07Q+0{2rRLL(ZN%@4@d?k*^i2VV%PsS=E znTJ{8`wK@&nW3?`4_Q{kvBpyyP;DmAvd#N~a<#hAwRk$}oDx3KX1|B-p3K zw6tdjJcf9N?+Q}OebRDB#zvie;yU!%?8oy`oXJ6pKKp*1PEL&MS2nLV5+x?2poah{ zfqI(@CedU_+~ZoX*ZcsZpdAndJ`=&k!92WP@AjqGTM^u}5a2t&OUnUHgVlI2?hyYc zZK^+!b+F%vhePOIw?QCa818(uCkq|{gz!k=y{1hD;{lys_u2LL*ccg&KJgoN2KR^f zjB1*ixXA=a3UFOwPUKR;@IQoBd;U`-{)%dM=J}CZ8N5z0=EF-`9JqNeC;Eyx?qVzN z8dMbSF1av2hjY!x9{qbY-`(^p6s&?6=R=Y1V3h{eL^`8jeYGHBer1GWtUnltLKZnx zlLS@SWN4Xaoa2ojJ3O8l@FD$3+{?Za8EEOYHoLmqNBm%$6olgv(y0$);RQq++pICzDqir?I<1M9+AdSZ#($IJ+gN5R^)Uo_wwiBwF+1I9%3oEaMKb^V<t5Qw{tobM_6?p~bOXH|@5lBdgfnQN*BbI!hKZa_5duxr_*f2kjG# zOwVL(+F&KZ~AV*g2;9a5b`hpR$5vPp5?ssED z-bjutQjmHt$ZtP8==bctK~^`-Yon_ccvYm97#xV}{F(l{uIY1Cny*8$Xy-EJ^THfs>SXihWSUpM3pyt6D@i^PKT;VqT?WJj zCy1fRDQcx3_m(otuOF5FUFaSl>@AY0=imE7^02HyF;tP1A+I0mJp`=m~!frV}C1;k6Ze z6R-4A$c~{4Ee9l)|2s-x*K+W#K%*y?u!N%oer(ws;ud}r#=R{|G!-T0py+zpe9e(+ z^Ih_W{+h3G@k8F zLTBvl2|5esR~pJc(}%&@wSxPbNX?Curp@Tbl7|LR@FT_$|6u`E9@lAUst1;cUaTs( zUY7V=7!eVp7wA&R=?buX83n{SGQBmeJ|APf{)6ou_}cS!X7XtvxcBe+86rG2kk0x zkl(zim**xz7c*Hehy+y&F$vIz#b@yqmR1eCIGCHC)v}}7LkvMtNRL2DfD=Gb>wv7G zuqXG-vVAA*k|kVzl=677}t!IruwM-L4xIszs5)iSc$!wWfI{0*KmlwG%X^{m9V zO&1q8hG_&?oZ;o`?{?Ej)DTI+hQD62oBv^T2+#KxZsb*$k+;^o_CEgOb45n2o%7FW z2LK9AC592Uk9LLzAytrd&)XQuGTu@Db=m5r!3<#sQ=lZwjrC{Bzb$0~i2406>U-n) zqgfVW_^CSNd&TX~nWMh#B*`Q-f=)Cp(?p0T4Z&U=AWI_t06sfj={yq-T@)fxJNxwb zS01ub2YgnwC}Zqu9h$bsI)>Ewf-<+s@j))PQ+RjfGKFqP{UF9IeB$*Kw zz(%t?CGSCcm&0T0mgMhhL+c+{UoW=pI@ohFQzzdt-h}!+BT?d&ypWbS>r(vT_qn?sL#-EvcAS1_{QmU&9mdjeap(7!_$&I930fcPQ+wUwe-W?U0vm1Ll~SW>hA8Alb1*5&j0r9wGi>m zp!xm2;``a-Lr<2;a` zL6AaUihkevn6ntK2k&OX`$Y((kf6f&q++;a>Ak`Sr3y59AAFI*mG2Zuf__^=OY2HP z!iS#MSM#A144d#iBC7QJt-`Bb7Z8h`-QB-KO*%a@17lQj^B^~Dwy8@D)s}dy-}mlT z4c_1~NRAWH`jv>Sr4AdlDMN&jj074Jj|po6mNi7S1hFyy=sjhFLrm>;Zy z0QaCQO<6=(QSlbqe{~OZxYg7*4jmZ4R6;V1gaR^8Jf2!NwCnKU=2vt-3I4U`g$oLh z<^8;go({)GDwzdQ3gHFxlVOHY=(FP~if`BuhMGnva1bliP|_$&L^a8P&>4QZbw&o2 zLj*7D(({TsPy{gyeCCAjB;YN8^X?b0o*iyPy0^E-ptrGXeMAtf89qJK<3o;D0lvsb z+w7DETbxQ4)6&w;w!;98xaF8-9(;@^@4wj&>a|xSWzI7gi7gPKK#~BqM?UF@gnT=_U}Y@?zo2NwfaZ! zIECn3yXMaA+n~qn-dAXRZmu4aduE3jJ7SHG3vbx)I8^({OLr-Zi`nogQ=ckKO%-bJ z?vQ~#^x=sgWj7LM*J-FN(~r>BUcU2v^EQUh&+Yb{cOR6HiaWQq;lUj>Q6_G{mY%sbt)(B#yon3?+O=!Lbz@}g;3*=mCup8^B6xEsy}@Hcj0ViY z!bY=Ig1;T&fh-YAym$(#xx47I>GbvW_JZlZ4+6q=Fq zI~I;I5PP7sh7&M1$)>NHxvNqZ84cmizJ#NY+{+Y>(17jsAUvqt-$_Fw=>5TX=!Oob z^}#gy%trqoXKw;db-R86t7K@llahpO%dDal$<)S1Wk`b( zDbgegAwx2hp<)|T=EziuCNgUv6@`*gW zpZi|-TI&w#Qtn+!Rny?62aGK6dC?cnyn)V>LQzDJ+>rm&3E6M zBJ+KnHxg@m@1I((?S{|WBkO9?cL!pKAXq4!kYz){0V>XHA9aJ)7$j+{&p1wz=lJm(i3W zJVVsfweENVTc);`_TN8-AsIteZfkGfiZ7tp!t^3KI?h;N!l5x@#^Es3-r2uI{a#7J3ObiYE$o{lPSr>UN=&%As0z*Ao?*hlNUG+nv!Zb z#XbdtwcSPDj72p@PaC%$VBlAwONA(`n4qAct{n+eCWuC6lZh!|^b+W39Aa+i@5O?F zB_WD4_F(Te1ox!Uk zXEuV+Q(IRUcmK$`>GQXa*xT*rVg&}8JTquEs=DqS965&rSy@7+NKRflutaI2Y z>m%7YJA6O9yY?z>eoEUp{ID&>)d71a*^SjyCU4h^h-f zZsLEiS33V(`zu}~F=QOt)2E4LR@@l)wRP2#>gszdZl@`8a&R=>e)L76ItR%TF@ypq ztJ6;qSAd}chG~TqoCDvP4E=#j_<)^=jO^V=q4`<^R0<%_%r^CynHs?h~V2yu6&M@C-+e&`!>D zdFH)))j5YD7evy&ld(DW{clg!^W(+-5)<`@W%CLSWyiJ0=dJ0PD%|vQp6Sx!YOSWY zPj3cNq-*S%L?H>D`i?FINb=+0!EiM)l4zQVrj9U&K7Wpe1rj;9NJGdNEnqUOxLQ=t zGxj{T7bnn6dAUC>>REyjFHbo*B%vUKvqvlh)x_4F)IZ$a zGMToFB4~yn?o}lFgtv>p4h5q4F4}$TPvJ1!>U^Lc-w66MOWxqe&6RnJ}m6 ze2|nR0yZ};26}ST#h))%7lJ>nMxdB8XEfvW8MSbRSqX=Bk_SRzyuYgt3@Rd7#cNDX zPJ-m+i#+Yn$&(dua4stoyPD(HStWw(*#~rU;5Sj592a=tL!idO-D`=MU7zb1i1!4R zCXD`y@S!n4TkD#o{#9H;#IfXE?ix6NuCw_PmFw8UBS}Fj)=5fMH>%;Hf3jay{jo(< zwKx)CBo^b#;i6oe*ywM~k&Ra=0OySYw<^;T*`;rU=~?aMsFE4`VLF6-4Yj5+8A5bf zb4Tka0`*ary(ZvJ9tCR?L{99na4F*w44=ca$wx96!w3V-L)O->@s&46)?tp1CqS@S zv#I8N&CgiKFgY9$q+F*?pAKegg{7+~u2lRLIXruE+jqJ~Zq>xW^ee^7A}5)T5)g`8 zS%}eLf>#cB+7{IwAU&A)z(VZjL5&g^-5i0v7wE_q0GFX&)#-VFau?f~GS&_jcGXsI zDDh?CEkW%-;)AuGwxmE|A%in(*q~H@Q{MVx+^%$8C1L@A6degUl4mTgr-=;v^sL?R z-EfA`2o{3C0^<8w9!NJ@y1N5*rCVYXV@OUEo=}W)J8$c0g}t|VV3Q8kWPJ4n?9Xa& zkPnU>=xr=sf!XE041I|({3svPc%dK8QPpWi+G%4&X1=WF;|7_hk8%m^vk>=JuqF@B;?qt}+4eE1sp1&M84sB?NetTFBO!*}0y2oC&Y${g zt}dK6PpWSiD|ob_Am3;^n52=Q-@+b4QFjqkm-}upSYXo74nr|8mxHi|gg=}5139Kf z#p?QV%}7AQ*~+o&*ByU<2_w^}9Sw!&VzH&}xsIHMWqRV-Wqf>#DU^ij4bUfutTe+c zAb8iX6aksouwg@I6j?rGS8)gApF@;Bf@x`;Fy??U67A2T_TGGWDd1_LCj83q1a&y2 zY+e`^B20{eu(8U|`{EG82_@K3_yVB9mK;=MdQQixukvG29M9^0u2B|M7dUsCmkNTj z{#dr-yMQ2R+2HYp)k6;=#ZNue^x<;ukXJijo3og*Rlm`J`b6C~V2-FRuT}3Lr$nIt zBF?>G!J)%q2_;)N3%H&R9IwrG|Gs>K4J%~{7Z4Qgk;oTpHtBI)T-5kusq>h`+|LT= z^iwJZB2QEmy|1ngA+|}mcaUvZ4fi%xb)!hf;hF(*LZrPbz*B%4Gc+_xp3E#NDe(l# zgE26P_mWj%?aP8n27bgkieGZgt-_9I85SwTRqkoa3hV>-EX*X~}O3gNJnP9Zt;5TE;mhkX2o=I|c$m2Uu zHd1w;Bbjk;k6cwbrwsLw zYGUgjPwf0T4Xx>85+BGY)A1AjHMouXW_JCe>5CgAi!$9*&Og1%f#;NM zw`tkVV|m4aGNjA*pybjs=ORR&z~7Gj59FB$8^9lRmV(pxN=_VYOdy4_CcrIt3mjSQcc`5*NHqef z_6o!+_xDPZmMq&3%O#vK!Kcp+2jA$@Gsm`+0J*usZt^!c?M6{P#OfW4I zsG5vv9tSt$0+`wNpkM)UAG4Zj#L3-Sl@(Y5bCE;GkI&_Hy#ObkHpGmu(VMn<>}XYV z9_16^KevY_#ytmtP91b-{ku`-xYH4gf0oQ$9TXF@9j``^n6e-t#7#x}uY%GMk$5V= z2PHyKBJ~(!7w3Q!%>q-)dHjl@cziRS**tXWsu9nuYd(ZSO#E>6(em&saiV7`%9->D$Wx)XiCjL1l3T)`I> z3IKszpfArPM&bxqy2;z4o!Y{76MGnzf7Mgc-?srbE%KN#i28IKmz9kz+-b#u ze=0?7c^FpqHC+@qc=99$X}Y0nx+_zc57tDyN=|FQkcaW*T0HCMq-D4V;Vg6L#EGrw z{p=}l#jOwyv`AMyZVkBn38*SiLv&uazRC-VtxOEM!B`r7FsBtvszjR8FZzJmPwZf`1exi)z%%g(+3R z2Ys(_yz5r(?_E^Iy_U@;Bd+<47OmAvJljFew|R%^_ahs+8+fN3ZL_FJf~G`m*7IB#YFCDRxlRtD|@mfu~sy2WgL>)WqiDy zwxb_<0`p_B8FwBVId_@INTpno@z$UAMfWVLj=dP6AKI2(R*k~<*nxy#*^PF>F)4|`vDf#--~%qIQLZ|9N7y3Zl6S}r z9VJmjtLV*K+gEN@RS{*+{}FM=S4gTgy!ex}(`hWUY-naZr}*PiHOJ!f@y%oD%xDCqp}9{MIttK4zNfZ^q**t26?X^3JfbZ?dQBv>&UH z&K=Vns=TSIKe_Tq1^Vy0&+}tx3tjzb^d}3ItYo#La+Iu4N9T^$TpKqPl&7DOe_GEe zb$UDEIn%tkw)NU}V-59d)bL!Uqt0e60p=op=Zp4zw4h|X=l}D4W4CBU$0XX_TF1QG zbkSzy;9FOH@w$hWZkb+B*pK?vb4W-mV_hGR<Wxzf8==ygno!{Fp(CTX{s`cWFQdF9Ne#?rPGj;%e%B7>DMv?97^01W{6pW+fm!GgdnlRuT%pv5&o{il}XI402f zXv5+x4bA#K=CcB`dN$`IKkuJbwC_3Ywv?OIVM-& z=Z(5F2~CbF7WuVnnQC5a+V^H6T%i`Xc$=V6b;%o;7QtuHZ9pDNCdT3Bx^??@-J^`S zg*^?0mAJDn!@vqyV>6ltaJcsbYFmwz59^e;qY%s1($MDoQ0-{X9p?jKOV#?Unqc9PG4Vx~$iu@!+I(V*I03aihkgi_ zJ*Q!8O|^z;yUbuV`a?z)A?^Yizer;BH&%yd69^fF=#RY-)pVIf7L`g0C4xVDK)ksIya-4TRnd`w30ga9 zE!XkBQn=DD^#DxL3~QlKC1K)D3U;6$jzX4glRA#FWc~)c6JVRB2S1?-PKx9Jjvsnh zR_Pi7%{`zvEP%U@l5VOXB`PXPKm8q1bfK+)8BHiwVbtjjIZ4S-J^P2ipabG+{XR~PEN!C+uo7ik}8()Z} zGu|8)pYwip#Elc5zPzuZL=LUGq!z+7X^jG_hG)|bwO)Hcy81gqE*BcZ@HsxLOW|YzOlOjN=CXkM@OgsVqv%9Cxv#fZ?9AmUJjOT29-wcbx?BwIL)CF= zvruIPM_@du}k=G=K*K zG4Y-2v^hADBn$w3_k0=NiXp1+G4CR zQ$l3A+Igw$(m2kma?7l<;%@P*kvbP^=8zosTDqUHwBGs)Je-|=r|(SIUDz^q&hlqq z_lN8s>`EQ4E#JS=i=1q_xOJ*`%i-7K+?CFDb=fE9Uq5G38b48Z`g?3)8N*-JX(}wj zfA2?I^zmUN;Nhv3i>Y_8AQU?_F?K_a0>mGKZ>#CNl_4cWkb;_Nsx80epenB>T(<8}~Uay1pZFr&MC+8%hE^V=Z-g z);d;LjX$&!Ehy}1E6=H>(b+D+gh%yTU5X^y5uw3Gmy8u7fS*X8~4o1i;Nl{R=JoDyU zjN?L-|CW{-V4D&Y=H`IFrZXf-A}b6dGL5bcaLZ7h__YJi68AXbL{-{ONjRNr>+0S> z6->q(Ugb8IMzi)%T(FY`(qu8?N3KW3n7Lc(4SIF0r<4 z3FqmYDDY$W=7sQf5SW3Y<HV zNt{&y%@ z;f$*j3(CvA?!(0~gD>y9XL)iOz2t|rO5bq6WU`>yR2qV{M1(>s~3JI?nP z7l0M5I>hiaPczSfFizCpnNKEnTu`Q}r5jRQg?*#O+G4|k-B!o!E8g&wE#QH7+9FOB z>yJf!!t80cwv`61D6)E~={011%4j!pk$^xTeDZ(@kQ8N!i5#vIx&(oq)&7Q_4OTKe zbo{6?bDTd|BRW|ss_f_5V)4Ox?cyeJx&S0*(VvQ+YJ^R^;LkKL_x3TWUpd#aOV2XE zdv?rEgXcyQ6y9f_^|6hQ_D;HmmUDAb-)#UUlwy=dEkzO=XqdWA^)mg~!GF5{Gu6J| z$fM2(qrA&^Dp5blb*e)ib)`k}s$M~9E~D8yDt8VwGxGS2wUR&)8f2%!yx+R4)m*54L4lg#*2LmZk|}#7cF<1LWnXzy z6u@@;6M0I6{@T-W@qg8&7nP~;LAOFTHYb$$&YRn$sW0fL?wP~I&A=)&CpIreY&OY7YmCAf*^BM{-Zk7==QLKZ zzi$aw@$Px1f$9XC;Cj2^Un}27Lz6f+RSb{;x0a=!xAndar&_8S*lf%ieeQi5cD`5K zZ2%Mv<2blb=`~5G2F~TamTbu`MCktGp8dok} zxFCC=hz0JL=&15vRd!Q6h7QaWKsrLE#(X<;y+Hm`tkt5^1w&r+{`yr@xRz}ze57si z2g2jig*@@mr{NS$EZ$e#r=I1BS@aUn5A|Aar(>1qy-P@7@~XWNgq9XUT2(jRS057n z;OOZ6BDE14^g{l>8=^P=?}ljX-JO<|PqJFHQEdi^TMFKtgl?6;0<(ten*Xq!y9`QONHP9X_lH^o|UXr7{22wlrH_kG%X4>~GPM zK?H0BoXhf`y}~j)9N+_Y1zfBskDxChM}JV}g+YwqbWJ7E@nrm$H#1WDPy8kZLVPf`lm zsbN6P2)zM?2J)j{$Z@gXBB|yLKfbetacV92)>IV3uNTb#)vg#BHI`#{-M}251f$MH z6pA{sd3buEfx8fcu(WApddaNrKUt>8a0s0X04xdsn32B*HOoLE43!gWj>&=|m!ABK z$;no!R06$Js!Xl$Abm5G!!^eq&?rE4|Nr$ICv)wA8NEmP6Z&B_Jj_B5Q;GPXvpRXH z%NJyeGzCoElSh%SV}1SNMRouuAWx!CO-QvHH(n(@;>Jb~og|6O)w;Q6r((5n0xFe$ z&O=6adl%sWp-V27`TCD_TR9eqmMDdfPZxAOa7th)!o|%^$8W$V0^h6C%?7)3<`Y32 zZ8I#g(W)?U;DU8or#4?4{H~dFDK$$zHFWKGf#Xu;eezv3Ns9Xa)M$)C>e6;hN+T?o zv9qwoGIefSLjJ+C$3!D(#|dOryl}Pqbr$xrXzNfw=K(r_KYRB2^?a02D0)kA0FGh= z)329jOifoxgb|-z?1RM9Sm13y&R0rO)Ya7!hqi9ph6!ik^pZcDjP_GNre&S`EwEDd z;et>)Jlw-%35t$>jg>UcwiNd48;qkCS3^2 zQu}J~$94>p2X`Q;1t^jQFdUE;FTov&CG{qb3iqGm`7JV^z-9Ylr0|N|@%H3zUj`Q&2-Qm7md3y9^1q9s z+zN$30r$S8TXq=e@&VHDN04N}3|!)A1@u1c>E;3_f(iw-aY(!IX~03qk_M4 zGtNC~ToER*yCxYW5fT|`d=vd2*`SwgV%+|xKkozP zU$iDABm+5}=J1b)zb*J2WH1e)fte-Uy6qJsmxS2|ZGYSsIMuXapg`-yhxrxJgDt(2 zW|2SZPV91WBdK_~(b6pj22>#xl~~{wq^L-LhgR%b+-2G{1QuM!U^fKj7hrn(T73Sc zE>ma|8cwy()Vg&=mIlwGx9!)^*E41#WgGy|(RkmiXCGQMya z_Curs+Lf-oZCete5vaG20s%R;0LeqOcq@_5ROP!k#vRz9{r}oI#5kYr9eV(Q5$11a zXD2XVlFP#O(D2wN98EAz)o#3v(-$Lt>o{Ztswj}^bjRsHZWBA5(fXrb8y*2*dqT_f z3wZif)zvRu3(yWcZ~iwCB?KjqPoijfj_sh{!-@NTt0SbF#&${?59f%c?{a_rWq;w_ zYOA>}j@B)W1)o>cKf0cu`%t@d*X{9ZNIOT|-|2*(%M7(vL4G!+EgPKhD@}Lj4g=~8 zO!x9eRM*ZB-FA)U=M6HYg>2;gns}*Na7y-{9WA;c{_=1D;4}hfg2D}I0*z^l$qG9qO7G3*6Q};^9)f2&k9+Iqsop0G*KXKAST(P%txx}uG#_Q?yp>AJaJ?eE zT87O6s&>`b|ItDG`TbEtS12B?^m6$Vdn|VKIX1Xyp@IS!ei($+`zPusDk_DPPbB04 zw5)sg`1IcdJ``a(iP5u<CVb+)$h%dIJQk*Sf& z!ctS$Orb4o^95zb*-}E+9slTU72~XVwlG?IpZx88`@iM?5T&niZr-?eIiL9ni>HDc zmdcF8rer;w6SLr-H?Bl&s(2V-V0v};s(Ag4PE+RZYF!^4bk6JmRK|2${qU(Q)idMn zU~X93$&jJPJ-o6XqQ^M76dh6*7`0vU?r+6Fos`U3)fXdpm8`{6tx;{AwSFvPcyF3t z_$vcXGS0D zE;M@qm~d(W^R7fjHw{?UTo+c`DM;ExtYN|RH5<=NeA^bSIMwt%%!Aq%_|qxi)JhvJ z{?pOpuPm-i6kH5{u1Wt~u(w^Y{pRz{r#49@%sbyymg`7pH6>@(ZE)Jpx_&N_lQ3=P zaCz^Zle%R*#*yCY#`Kc5LcN3+44KSJ-5pcS6XHGLx!WqXZ%-5{4u@Cfu0cczQui8hs~k%hIDvEqi4mxdai)ZgOga~}@By6L>e zmq(AjzbaRx=?qsTHtx$vN~xxB6i`%u9)4X`DK$Ls?B_kb zs{?P>86NC;lk)cExLY&FwM!Q5ucC^3g=RaL-RqKEzTs?S;S~Xi+UJIzT^DgjxZzXi zM}|7ydsbr1DRf6|t$PRWSXZRx9dem1%*iC&p}eqcx~PQp#Iq{=5oYX8?4USoaU zY^HJN3f-~)ZtnPT11IJ0T6YmsIYi^rYHSyKO;4R9jB$#DYhff*KFHcNk)nb@iAGH- zBL_Dx=9L@v?7vP|C)*sUOB?=40%PEpW0$0#$Kh?)^$XqIVqAV0BR~Wg1hkJ2)$_}y z;ZJ9zdDhMEs2o18;|DO`q+g|2kFQp?=s~Rgc9Y@x2s{gUUl`mn9#|rLVZOYRz}y(H z!v8Eg#Y1P5&$W9=uNUxg&`{@8k5{6?@oebT)j{RA@wEvsc{wUqhF`4qDVJ%EFBypR_6W9*>g`>2nAA!bPSTsCq;6q*00aG6 zb>X7;$)j70#iH`xq$RB_Jh8XbtaM=?fZ6H?0bvU+4*sC^mhdV%cE#CE)JMwg*=WOK zuC{dThU4rKA}@I=ZWvdu(kN&6=DoPK(`@a2siRI3|D*sHBP5Ktn{q#e>(X%8jSK9B zCdLL@9yC4q@aQGG5oBlITr#o8b**m3RJISe{(L7JeR^&K@H@nVqXZbSjA63$qEFZI z^{ZGuAbJ(jr4?`RVJcjvqLiOYe$MT`U2DiwaecOg2w&};gI&2TNp}5JixnJ4m!-@J zmv>!fomD)xrx)Bb!8b(u5T|9&_QpY7U>+7bVt9%U!o^ z-+rY+(;v6IWdDZTDc7x*S6g{Ec~1H^cM2(!y&zS ze5qvKRPOR^nzC98zrnyS;>Ff0GP`KFt?#dsFUiVR*syxcEA7C&=RB{kpPIZRu<(cI zU91xJ`&)#EI5=3E*v>D`c*_#X!pqM-Kxqek!E z&8w83*x7y@BuHmawOEj4BTK`;*&B9OoQ^#yER`^%SzM=ertaoEv_vdx=kO zj#g>GJpN`GX%H4e<(eOuzlt0=_4YBXJaVdTtI2+oWqlvlxSsnYoDZ>tuWwoPGLmP7 zt?$kF+OKSUF2SVM4ZS_2LvNu5bjK8GYLAwtCQoEi?~xcyg5`0O6u}EPSC@Lz3GXUa zb)k1hdrhOeaAI*lA?k>qW4MD}qBXU7yQb!|SJyzBJoimAfYkcaLYzPTuIk6v4we)S zAEV@A7l$;`hWZOX+{VQ@*Ii-i!{+(r%X3V&pE)ylsFI-JYgP~-2HBsQgW&h}$Fi8Q znpWMIulNqR6X@Y?(|N^BU+<*cJ4pO7#`SDMKRT{}RxA~Flgi7M90fGxS=TeW);VQt zclaG;{hePpD z64)n~L^mrh_EA4?BD4Bu45v?j{4F`IIRwc(u<^%m9vw+V&Q12YtoTI7s8AFuTa7Ou z>kStIkdupxn|2=F>Jr=fby5Dy*ITbVud7R~cUWtGj{EO5R!e+ZNl!97g`yY>2WNa5 z3<@6Xoe$FND(G6#g2zYaoI6*tI))=q^FKblI&;n0nom`kuUe1X+~yYO6Jz&F1}g+n z&&TCB<3MlFOM*3u@1;vLF)`n}ch^8eN`50F^CL6RNeV14iQO^gns+f_5{}^`Fms4fn_;MCEq=JGw~0+%*y5EjtcHSUtKo% z##Q-&&F*#m!YQFE>I$#Fz44{p-IWy?$@42KeHxnF;v^Pv3Y+phpBU!&@yus!(d)1P ztEo_o06j~sPTpqud8olt=S6<@niZyQVF(d;Q*EF9T86DF&!oy+`0*PsP6SblY7GB6 z1J}xKxX=J8R^>XbX3Jfu&bxR{nkVn<$QN(p41*Ur2Mn#+c3|p3tlum@W|=UNgvKDc4nP0wC4%JKy&Wny1vFI=wbepq&>x#a3Zr`EhP0#C2q(fzW9jij}zK7Ag2L7KRecZa9d-x}hNnq1*M3%$nM!Ed@ z_ZjY=_gUayxcsy}5*-qntK&W>Y4kH6_f{)S|E9Z`^{q%&AO~KmW z_$~F3o5mKO89i|jTOU_om^LlW@U%iB?T*aE3&xd$*niDkRRUI4qB!>g=u~;ijNYrZ z3Md}CvB*tB9jai!1TH`YiwndR!pF_?fku(k=g;2-~ z|2OD&7N9$=lVfG60U+enwze4nS}(&jKsNFc8oaxH#RlvQS7+XZp?`EsM!~2AzK%?B zS<2xya-r-*8!=6A$>+ndi{m%CtbE=5t)x8RW;j%xiH zBvELi-YOj~wf8{ppo+ipv=XAsLRRt2H(xLUuCTMQWkF{J<@{__08o&F#G#!7HwP9p zNN`;WbdIA1f`>4S&OA|GLoQ6X(D)JP`_iH}V5qRRKKG;h<^V;Y#|yz2`!}F}M;{m1 zeNk&iFyks*((yrT3MY6xpx=h+`Btd6j>2&n^y^iatyeVyYPcnwKJv!P? zFp@eRHMrwV_kol$3mnE$sQt>2F6%*T2JqLWK^c1v&8ZFmOE;l0$3lY4w)>`{(zRxK z3HAYgye2+#Q^{-=Fw($uB%(MX4JHO004shr7K0CHOsogmV=U;E z3*g}efwmu!x*+f-RMxRjc!9gOK=55ET+FK+KuW;5%LMj~J?R5cD2NMa*Ffd{yf$a%c2INf{>`zy3dppDh`vJ#V^07hh>B?4OljjmnccH201`ihmed(7dG$(}7Hk(1=t)?#?AJ@|N1PPp#1 z>|a-pUh~m4KN%RQ92uUP9jqIEe6;N9+>{h4f7-kCq#Us zsA?It=*(U~)E2YJ@d`Lv@nUlz zZqa|aC)?J%fGV_ZozZ5|WU>}4Xgm<>J|aFsixc8SFyZJl#w7|DC;soLugS#}T;}P^ z8ow@t+a=mgv}Cxiupw*Ju-&dZd!GLqN?v=k+2s=7rtnjTN@elOt>?}ym)!08e7EH8 z-j;KPZS{U@>`!g*&!5l>5SpgPiTQgbCnrC)U^#%!epllLIk}Luo#_XGe-kkp2FP0A zfS>qypHplP?W?^aC`BMrVw~zFm@1@XY*rK|!(cQF$k>vg2lwM)Dg)FzTi@Dg-E8ZJ zUqR+f5L=<_dbPoQY`%s86u;@CqljED!718!s@Yp&2Bd5?N%NJJm2>DN=@|w731Y;1 z!~ zq_}W`6UyR&8JVeLtMbNv{p%XMeOx!}dzEnoWsYh75A^e^z)}P{r2y)eHVPLJ z5g&=2Y5MW%vxyd`rXiDVc-5D&MqOvL$c*kO7`pAwRPW5D^d($)e7lBK;ZF!uPg~b{eY=CjMGtG#Mvw`!-Znue3R3Vp0y3d>? zV1UJ(RVs`zEvh&9IbsUD=u~Kt5%Xr7X3+bkxR?jLLw}twL~Sc}-IuXwo*Mb!D`On)X=pdW zYogr*@k4z6|M6{f+A>ipg_g9IO1Tsi42K%BVHV?r3E@Y1S9$rewzPs*NBm%r9dSGE zpaoL{d|8DP6Bmwa&^XlV=x|8mKxB&lg|($Jmo4Yw)MOZ#*)dYW2dmF4qNGU&_tVKY zwmuK!=QO(=WN&Gg`AB08Klx%O-YcEDb{%=~gq6cf9sc;v_R7y3_)e$J>j!ND4k%8) zlPw0m7_a<_VsODULo1_J#ZH0ZmuQRqJ-x-b|9;0$?)iH!R37$#bUmL1#WD8zbE7N*Zz&icv8ntbLKqS!dH(!)&D3=GtH7G?nV7>wbtJl!Xh}Pz4Jo+} z2jSYEK~!q;HecqcVyri=Q)lL*zEcA*Fg)E<;xn`I-LVJIB6cXmFqxRT_CReQfDuIA zwX8ob`9;&8v z5}&zn3*bRkiVyz`c@mLM?21+D*ov$G%0M3IoVAjx*Pi&W2DM-4x`dbk(>FR;J0oP8$^%fESAcBcAh-zv1soPQQ_bg-XMsp-Jt7MoH+?5I zYr(9~NShXeG=jNf9CPOiV=f>1ameqM!2koaAC_^5t)Xz*Ix!CkLW;Wpnv|;HAMg>~ zPkhFUkyRlSQ-CTb>$8htZJc75GzaZsb(zSur<&=c*;zzLTF~~sI}1h#KrS{y)t8Z3 z;OOW`T5E`29N-((q!{3+V+3D`=j^%tCXOdgNWqK+>V@-IS4f~(FdziA7;I~wQ+afb@1 zO-H++#a)E&Hqv+oMl)1p8;K*}O$aAg(ik5eTJzzS>>Bv^@gtH8Vm@k=dKWxt7#s9| zyGRBUVFSMDv`6ec1Wqm-#4y#0yleO=p$!Vc{d_WJXaZT8FJIz51`B!A0$Hj@l#|pUs3tNCUy77HP(2VJT zx-<(5i`GpOkK@QgAT?l_zx_srYr9o*qu^=D`Wvd0mGC5fGsUSPX+c z5VBBS+%hYS!NUJ_DdTfvwGwa`VRWI+{}gH?QgEj9Sbw-HgabXR2!<>07-K=zK;L2h?0*lo9!GvTSALiR$X4eEPTpp7u z{rS;s4Z0_nVHNu%xdl&99*73@zMFGyzsb&T)?8cJNwRu&4V{K6-V0=s8*h)K>`dnY zi?QQ1q#9|m$|J2r5eF&^s?8m#h9Y2+#q6aSrSIAnz1;>*1+Z3ue}tZkw(3ddA{eBB z!Y9FIuNbYkM_NpwHqsGC3*|)WIkHj>15;=7V-R{}aSniD0mMB72d*yr(Ql7Kf`j{O zn9|d72J(qpQ1-moO;g&oAAFqz5H@nYG{Bt}H_;Jr+aA9X-bVwU$0TfS(0T@%GdB@L za@@6rSFKv5H~nmWh}wdP=D@~c!5M|40HwaKmzUSsah&fFe{UF-HGltp*)-$aiO<9b z1;zanP#t~dE}c1h;b5*q_j6=HywHh)JJbeAUOd1jm?J3dqP5lw3l(Nzkm(_yCS@Ri z5J?gdRtm0PABKiznl2X$-Xsp86%0|xbhvZpre_6 zd&1OW4)M-e=g3o#&&kgaKrTqBSiO4iA;(SRHVBbQ-5K(&QG`aW2fV5i!(vw#j?0RR zi>tm@?9obg=y(OK4_b!gPrz<}b$!D^4`dbKCysr1*vdTvr9mQ`0Z{Ju)>*|aN#j>& z$f)gxXOAU}lUYbT4hgK%!>A~)aeN!ZlQxLsIj36KU}d|@QuPdIlBaBK8+s{g!nMuN)-JBB48#{U>+#o7g`ns`5w#f3bVz658T3k+67%rRIK8CS?A$g4 zG1643IaEhA$Hjz%Bm_2SU`2tn(+tr>-mfa%uB=P=LVGZ4-3xJwy&^w6=ZGgyz9}&u zHAW~$s~E%@t>|z}16AKiBX)X_t!$Zy$%>HYHgbf=02#r@0ue+4GF8?02d416oj*Q8 z-Pl!|QHkdRH$ngeD{;GfC{fK3G*z4E>Y(esOOnHmiwDCGQHrG(3qj@uz}JbbhzM%& z`ZG8dE=)#XAnX=wA$ ztt@as;YDtO^DYNp2iNX09?qbF&G3YVnpHzzU%x<(WI5w>^3AwejS6=U@1t2P9K{eg zYz9FZjnxAoz8c6LG=3zz+Iw4GFZZ-RE0JH47edn22^yzFA|kAJZtf;bts`Eh*c`Kr zUkX7!jCcY#N^c40?05ON%N%%gL5Wmw8(qhOk{wjm%QZDMCf{GK^f1dkGruf&CWy9( zFUUY)UDON^O)Ql0A>RcfY3~yV0L5BW<{>R2m}PN?$WLO|Ee5oqLF=E~!2JWXg-}$! zxQ|&wK=u=_8J+&_7;&XReuAQs*u5f?2+^f+4;H+>^1rKxUT~eP(06i%rVX+*8t$g9 z=U&)vhO?i9;Nm`y@_cj%w@i&_3USPv`+rN>%;iZ+cgOrD!ko3t2G1Hc!4IM2c1;hONI$aWi(hmJ=?LL%*rH9kSuF>$k1!?PgehF@w2546O3D9sPT}E|tg2K@8c{(&7~nvHOnMOs8vvDvD}^@bmz=ANMK`3jKyTy{XyP_1mZSdDr=K z23pgV?2i>IwhkZj8M9it#AP3A^&eH9zafo53(i{dSL)}tz$E~;&(QYSzkL&M6B=cU z@SiXe3ui~fjerj^-FwuiO2Mcg&{`eu2y%$<{q>jE6w~P{jg112e0|;ZX>ER}p0d;qmn@O- zG1}py=58`4Rr7F3*g>0gec7@CEAsPnJhe2`2ey7IOmbLB8Coj+q+o`|ZB4?cHZ?QTjNVQ&kWx?fL6%bD~N2dx#VJJuV_l%ba+B9bR1tIqe0&+`NmkycpiOYZ`K0Y3EeH&X_pCZta zVda9LY{Q7S`VZ495_It`=&U!jw?9W}H+bu_p&z}U`}eYbU#n}$3YmLc-s*yK#;o!7 z2YvrammxF+LQ7oQaIq1K6kI+t@1Fdu2<&B+g&!+;?ZDTv&rKeSsEI`{qyC-s6_TtE<%dp~1Pbg$9!Mf3cHh|ws10$5fLxmk0oH@ijU%}nC0ZHW zL=}jy4Mxf&BcDS}1-B_x+(NKpo)IKM0jSTes;)j0BZX#??fwiWLX)8_FdRVkQR$6) zlvcm2UYfjsRGF`q)hLWuHy?h-&MJPT%fN0K*KPG zq!})FG*Wt@J|)vn=9f91O!g^Zghw>PP>JwEen950g3WBij<}tXOsbvP{?q;m0N`2B zj-ewxdE05fQBmoq86Nj;5mNugL!1n`=4zztC_|`LcWQ<>Zcov zNy&z2EYevDfcM*s0^o)~j^O6YV%yiOfl>*|hS&G%4!To`S6hyA#p45z&)_Mjv}F3r?0yi3MR3g67(so%WO*}^PScgA#Bx%$`XNcCmiF%f&8{6LPRrC{gaZ8K;W zH2+E5)fTa;Et>ZvbPOo_A8+7qRE_)z>ytHY$|%IJ%0~d135mdT8&D$olj1d7a{>uQ z|0nkK$EG8HqPAwhldAS<^tUrS2Z%>f?>49Z8 z>$R{HOvb#+czNul=@*@~_P4h~mUsFmE%Oy0GWd^m#T!)p{SA`1KK-g}2Y-Ln|BAl< z^8@@*9xAx&k8|vf|vwwg&Qy~AmVBEh5k1S=Pp{d%r`;o8wlDBwK%50vw^K8 zm36&Ztr&epe8;wS{6Xrd!<5hMl;CjSGJHF+1DB}|Dg}=`4Kkq~-odNTA zPLRh@w(bCyz^&1AdBYd6EcfORdP$Jb7RaW_)m_%*x(>#KHv!6u`Ktr9J%WN2c&?fO zH-HKdjl_%;fT02N*Ss-O`cVS&c7Kaoe=oc9E~YI;3JrWf95&#djTrIktQ+&!;#eBx z5)*Jjg4i6i(f~Bff@ShkSU6ls5F@f~9|_#piT8gi3;$R#;X-8{i(%fbx_8pygRT#c zten4peNE==*Za1xGbd}KXI4tF^cqr8GG^rU&mq%J1Z8C@ z8V2w-X(Z03IE{p{4;&5+S&uxbISOzwe}K=6J?eG&=ImHm1OSfE{!v7ePBM-*JK7Y^ zvNkHi$lS3!NjhOQXRJh(m16*!-2+l1YHFjdu6`#;5Pk$KkYZ&WD4yvM?iCog5Lx^z zjO06v7eGeHLeMn-gf++COR=D;Kc66m)-aC;Zp|iKW26ZS;S}l9p{9`9d!GY+kLu*W zj};hrjFkg^AUPf(!&JhC{P&Z-)XdF!l=G3@gxj|wnRWA{vX?qUdVkv8_w(oz@N(z6 zP66S?9njR%qlcYEdKVB1cBu{nyCHc#`T@ON>C@7CY!7G&*(P~7vDHct*q53*ok!hy zLu?L8YUI1b(h;kT@*baa=172dX;^Y~CmCvEzzr12BRV?z>~?q3nBvwjfDPx!VwI56 z{Wef+!yFXb2NxaUqNCzOw=q?GB%E^iy_yliN(YDe%9+^gj;!^J-sKH$$nO zD{gbO#zsH9zw_F61ADxl|Ffj?avP?`XW5?`?`h?_cJ{1?%#~+zRZ3pGjk$1V`QdyO zZRuoY`rLb8V(C-L`F3H2);l<+7L4jojJ3oqjFj(-p83ht=(_ab9ry=KC?JwO;2-hBqDD+`7lL=7 z`hJof&MR=oK(B~Y1p^-dcK1i`?@jD>vp>28>xy;?oXb=Aj7S#}YXdDmf$Ol%oO%!7 zwnYOG)jg)=%)%(?ZDM8xN+31dGR?mRU;JJ`va{}5{6Cbv2VBqV8$MoX_%uW%Qd%NA z*`+jSDB6Q4k%S}~w6~RZ*;rS&)q!-+2Jd2X5PaFhl%}wdPl&dh6L9+Xn7^Ew!%tbRn8r%G^kqNcsj8 zBOfl8m@`DPoRjkl>_KiOC-1}k>x4eZX%ZCZ)VKrF)@5?)WTMbevLaay=%^()&u`H^ zFv3pp-D2MM)gO-p3Wu1GO2Ak&a@hcGM}xSK+9f22AS}Y%GDfiZNX=}t;zziUX6%$# z=fD|{DGwd!o=2hUjds>5Idz-G`AIxUL2!|LUo0R zM*Lg>Fcl=yAMr=C2mkwhe7Lzv^jF-vb1!P@{CMZl$3uHLwPUYN)VR-GYuRCT(l$cX zGtxD1-P@X^;%9sP=~aAatuCla-=5fQRkh>tkaT(8ne!78GcS&-@QKq2mjA^CI3WF0 zr}6#8_L2d<57E{RXs}G?22cCewpBL73( z34Eq!77m0@5)=u@2K)~Z-@2QYBTYe8rac|t3mzYE-pG(vsCdH^@A&w1fc_qVqX{Yb z_A=AD7FwH@akvI1*1!680n;Pww@(ZxW}UXX;o=nw>XVB-V*4Z;^rhNV^^Ze}C zOVz=LckiZ6<$hQN^aJTTLN>mWkVeo#sIs1+LU`JJTxhOSYOxu|g|s>c@fZnNe2;$S za!Kg@s1TYIa0D=NP{^<;uo9pb`_8fo33@j`%J@hY)zrqz{wPdn&zZePuSR?8wrxjL zROl0(rimRGX^DURA+4^?LCiRoF6G~~iya3g@>w;Gn+OR@Fmd4K^Nj?ilU~VESHnW+ zO5hb#RcSxiqv$DwZ0lc}unn6~VOP{n+qvWAc`KY|R`0o_)tUT8tvpZE{_=<4Pro0$r34|%REt&sNQV`bE2QJSP3ZHC!N_w7EF`ODObeu&qI zh?3mm3BAhq1e5&Yi|kdcbay?H6?{Jz_t{?h9vwfj@M4(mL;A*jC*Qw)+l;v~#K?;Z zqy}s0ezbq!Z-#D{XKHs-r6=u_{pcZJU8s#h^@ON+#4N}VCN&%kI1ipztNSTZp^Mal zRuLTI0|D)kackEH8%wb2VFI`H`#248rsj9y3=KjdX{VvLZj6%SQaaK; zw8=n2n1cBzmCa!Kq*_p>b2_y&T3w(bg6p+(=~6==nmw2AqB7$*osdK6W_-f^#aEPD zwCpS9UEG};zyO!=4-zIsnGHGgtcO84c_yLpOAM=ov-ZrMy{-8^FR;(GqSZ(SU}UTv zsGf6MVq#wflV(SS+~^3;>HRl01THMboKY{wW0jaZrBiPiAQ?FwI#+yf(dx`w=NoQn zPd3FKER5nSGtT~~E^qcJTrf+^5EQW&0Fs8XSX=9D=Cg2ZSsPtx9h4h*X5MqeLkt$ za?tVR&QW%Xf3wNdXv1AWQ~S_Hk{DT7i36e-&cSv^K8<#e1JA3}rn*E)2 zW(D~&-`~Bu7vbp0SsEPU=~0}!`ux%0JNo)3`+9!tv~D?YPT~%=K98&9r?)+$suvr+ z-Z!sFXnt58y!)EDm)!l$Hv1-9TDl_~-pyFrjd#7QW}j)@`()p^P^%2{y4x&Ye#CA- zEMwx}1~JX*8m9I>if~gvov__?Q8X5z3S@Xkb`tq*G}!Xr49WMXB#*8s`h&# z3@6N&_01S;+hy;P=~S``N6PXW`bEN{ozc*qj5$5_J+G37WJml%5|`xJJ9a;vpY1<7 z`ZDDEhnT#!VWSvhv$%{0f!Nw*oFzHmG7f6L(ko9jwtR2mgH7bcAbRxa)H3Vd+LpEp zWr?bsh&QmnmQNGA^J8yPv_agO1o{d7>49VAdDryqZ>cOeeaDayOJV1k$!^30?uVrn z>%-RJzPj56qvDG=X`Aa%IV$0uP(hfu9yqO5{He!)$wvTy2hG$i#nBpxGX)c7;P^AZ zal=w55)tulE{1Q-Oh)0u&~1YA2?vg!tZEy3yCm#h>#u=+lJG66@Ct}-2*C5w_J4D| z-2yRzeeE^>kG*l z8jUzTZR;;s(K$1}}AY%G_1K9{(!c!yca{c+vs zvqed-+r#Q!YYuw&=g{K~{W88cY*zmS6+z0?k)?Qf1OTXxKc%9n%GwfdN-i8Pd#>}T zcG$g$^d_dG4kpYoljftN6D8QxV%ivk&|Z?Ava8^z*O}Sm-e?ouhxn{}^9CHWn)#^z zjnxUH`uyK^tL&;9Yqz|5lDoLb@dIIFhjDg&SkamW=c%l7;|+Ko8gjC}SAtJub})@6 zVGo33J(R=kW<4K6OhVG6y=y^hKHClbr>_P+#HCT@uW@&^M4vA6xfkVYadq-KPk%2v zXKG+^ZkqehWs3oS(j-ThQz5xD*k&~!UFv?^)seftg4i}uzH7tul438E@pNVvtRv8M z?jyzWyy471g`unFuOCc*>79?(t8z=)^6-_l@SijUw-?HJ4^CvUwui=oywO z3$}T!Xk+E;k$h{JAi~hN@Q(pG{f_=`>$;kv_I0%knRn*+4DO9K?rR!99dEF|!2Quu zZK}h!LbIaGOx+%C>li3ee)L!}p;SeBl>$;ahlOrg{MZrX2A1g^Jryp&C--sA&Tm}$ z(&ry=#NU0m4^!9Y*C6jq*uFkmBaeq+b_UOKsDnqXDb>PYC8e<>eSezC?U^45x39li zGV(d(XKkXB_skDI;bo&5HLGH<(&EydT3#q)4ZCO5*vI;4#}y7?-@XWxG+iy_!g*|C z%?!vI&z^`7H}~cBA3Vr}mi?Vi@y3RW2bsHO8M|7hwrvg4X)L*B@xHVDVX8)Dg~mz> zO7Wgqgn&*q3YOH4OH<4Bc8|)2axkUB-Fx<~RO)_g@RI6d0+$!B2XEFWH>Ny^3ikI~ zs9C!qV;34deHZGUbFKX`9s7HITM94hv{gOYCWp8uFZn5TDtR;u8B10Xr)6{73ZBJ9 z`{MK1JX3R(H+g=HEx|7=Kgz4nqsad|RL?_+!m?(wq zoBWSC*$eSBqz5DXNk1I-kUZCJp-qnm94jbrQZKA%3ma$gV?s7PrN8FQz@o`1xRdU6 zey8W?{6TU5b;E~x%To8pKU?gNk;{IWwB=&s_rOp4L|nJ~Ses_3$+@Z4SI?z6TE>^n zPM(&vHSOYWTqCX+d{=bLy9W37$X@*+dEc8Boq6Oo{nOoNI7mTGgytS@Jr_C z;kQk`ZiYRNEck%7{5O+Kpm{>OxHv?(XM``sht(;a4_QE4c?cf~V+d>O*B|uc{VA?l z^ka?HHHWoxTqS9V^y$Ou4==X=d?xZ>f!hi+Z$u|leTShi=re*cdelg?^y9H?iCPaZ<9wRO=-VD0?8qVmUM z+5NAkKA4qk5y_-YeI4r7+cl_g_NdU0UHRmU3{;pHzGTQss`_+Jgs2{QuZ4bxM?0I% zcJk(4#U>$XYnhb3;`y^);+6u4AyQZB6}{I~RkicF@vr{p4*i2=yxdfGdzA6L^R_Q^Spy_3C1YjtADa2Tr;T9f#?yE*n(%^Q}^&Ri$i zZkcXu&|s&5pQ7SN$NMhJ`eV#AnVLSt)lFIN_>g2e+O+6!2urka?JTGDWmC;$pSV

}ooIIFrw}w_Z3M8o_XVrMy>eV6@779}6}?;!PGN$&s8ukxIEa!5Amedq^wKQ*&IGI>(|{3pEr@3_r>zXNhP z{J_jb;p8JP55NruVN(otw{8}}%8_ukF?9eYLg@GETsHGpEWGqbd*Y9aCHVPKbs==X zG*IK%O%9ZxqYnGV)(GE&n;(2o6fze3XTh3_bU|+0@A2F~M@bY1V-NTz#<)FSHb)H= zq|HD|C-H>Dv|9o2e^F==H%ZLqc_oQ9Qa#ym&yDl%A*yKz5}53`iM{#k*dw+l-zAn)Uyr zl>gDq`d33lN&Oxvt_ib>7YEbcr^0rL8plORP1lm z1$-6l)1%|##<)x{-nq@w1kgd*bn71l$-mZIWVD_DDCRnHZUzbYFvA20 zuMr{7;^yv7O{RhY-zI!r2w=dS0Rt>N^*uE|_&~rzc7YH@I_#jT0n>ufIE@P=3Ft!1 zGpDPgtAifOe|L38U|<1k82&Ok?Ra&hcaH)YusI=qqs~JIjSA0Djuhv|-wJ}~wvwmuO5TlgDz}?`KLhb03KQ*l){-_$2$ z*gg5E9-J1cd5fVm;7UiEZ2uHiW({L;37es?^7j4W;sHTHW5xZVIv|ujLJ_8!jw!AW znO1F|JG5=4GtqwX3OtggYAr*?o4&q7&+x_Z%)p@=O|G2V{y>jCoZXxif6urFHyO;F zZ@uHwA){Lb&tSsQ<;JuWwa$)Y*9cK~rtbCGy3GGqbF++`5&(FiV?n_RN`){Jq7DXZQW5oE(-rgR7STTfS7}q5UJ4dy$>6*-iJdDyd zR7wjJoth?>w>w@rKP1r zem7mJ1;P|@sKN**gvId6?$vuQug0~LIdFp+MX3sm;=G{4fMJKhbYB`9LlDtB4!nW) zp7{;6vL0TG@$W?R)%R)f(Q<6Z_xs0EW&K_~kXx(9S(3eWVZ|;CR>5GaH-}OkLo#k${UNKE(n|34Ak|aflIAa`ARJd2ado`py`@ z{u2Zis8~J4f2CWrGom91i%0lIdSF?B^U{#%%x7w93bG|iqy2yK%Nb@s@;eJ)k?Qry zWFJs0sL%lfMB5C=S|#dGR3(`>2Q-sK8ZG4GI8&*ROBjOfV|y^!ZJ_Siq@0X3-0~py z0Yid{IAY?Z(x|;XD@W#G`6+|qR3hbeWta8`|EF^W1%r^N6VEGa#c13ly)(aO&yFH#KntQ2a@P=v$2dmE2g$O*xDqOC z<*Kr?hAtMRE#hf2xx7_hlPSzQdT z>BXDfIxsMR-wZ@_MDyvRW#=+t2hE9mqYVRY@^R!sUXQnlz0``|5E-&`+>OpA(dl4* zC23^C3)gRtAM_OF!Qe4pkyiic}Zs5+lW<4Kkh5?K0bi*_>v&g%ObD zm6SYF`wX!yq?_erohC1r&OQ$l2UYyHh!hhm73a63bK(2R#Bzb#cB~0MVoB(%f666} zu3ABPi7rMZN$=WV=vlXdf&m+9>!q)g$AvfHsuHzs|Nhm;f6(B{X| z$|@{m)YYvyWzRr}EjWBIWRUQWz>{S7g;7$6BEFGR63U*KkkF>inG2ZDcMm5K#=Sy; zxE2~Qq;JSgJ{#>jNqZb$h_av+8}{F+<(;qrG3>y2LwGU5fEa{`Up+l~(IuRi&2k7{ zp!nk@Np)w>koVRXF=^yP&NA4al0nLOc{fp$Ud7QUVcq%6U#(p7>1dOp(;_GDA34gO z?FqwFoM~o5=NpJDsz`wWv|KLwn0^GX@I`nRm3)$iyf?4H3wEpmV zb4^d5mzo*-@Lv9Kc4gjb!K~wx??@ntW>}De9_}Tool0cv7YRRm1phef7LIJg;@Uo$ z5#|(#uO2_m8{Rs3`O>A$NH;~Yt2s{ph65h$KpqAP+Dnpif5nFocHWE21Y>pWfaa0ASe}wNX|a)_4e}0t*@VbT;#8~(EYnW+HnSt9F^BmqSN)m z9x_(__v)jEe9v!6te)N)FlxyhkR99n-$3n0<6JG8iBAbi!mIn6^jA7-|K=9%tsBSS zKksd$yqhBTJNzw1V=&HhVsN&Ojz9GPwcQLZt{>y#htt5)5H&;hq#3^;ZU zj_%B>cTgcPKraR1phAc#OM-F11 zfQk} z3{&Vp`hYe_1-}^|ACGPC!y`r*1}WG3*WgjSH*X37fT8`{dlV@!v8(YL1bUFi^)Ig8 z4$PtY_cf?2lr}$6fAq<2iVIA?h(2FcmBC`rA7S|(Fd#@c=^3Ht!KfoL>KapOAW_+> zpBe;$1-3*LwG-H1;D&;uL?*_Kr$aUhQeI1E=WWoWP(Xq6ZD?ea14s$U9t$#-bI7?c zyM_p;z!ieJFSn^@a+Gw8m=Ff#l@o%f)ztd5ffzUgz)8e z_jBpg)N>^F6s<2z++Fi@U&5rGZDnD6U6qoV=(`H1$5nB~hd*Tb zQyVMLoY3M}v0P2<1U+V#D;_Qm;@P9#m?37gm=;-KRjqwPZMcM4q0ak(VjsPmkv9JfcQ%rt_ zshs>-@3%?2UDR`-J#)s0$*0c_wSk-4~-Jlqla(BL^38PA^!5>6w)w8y9# zX~j_M+u*E6)p-`t3QUoko}P?ARKRc9*`^G*~WQ}};HDAbD zT~<~aH#fCcmgSe2@Dc0Rua7*E5KcwO&i<02%irAcSOWG#tIFD3mOON<4Vr zgG@G(I+D1Tpl~6^3E(k4N9r2%-~rHm;%P=gJMh4EED|jp9r9MNK**4F44xp?DkRY0 zn7Rtd9g@s)hF0yz>=+5%hN>B|`e0O7)#aQE)-T@6Td$lMZ4i^)Y^hTV#oj?*@)c=CVaKffhJ0-; zA1@SFK2y5eYH>n9RElor#cUgc({m$|Vw2VMY>=IJNV|A!P&Z;&@Q$lq#^z~JVcQWS z{PsiQInPm{fY!4wtPdL0zwNDOV6Z<>9dCR4uJ1vb)SZlpKBCs1yEr?3c3m+mnPJsc zu74q%H}bXi+wn&?S|3M+yx1rVC{t>O{g>S_$BI>5y?Dju;!jhpp~3xXeYCUj3yj#Q z*V)^dAO7)Bbd}h~ASKzH>@hB;MkNj3NTq(DE<+Q;c{xS*{3OF+UUej^WubP-*Odo` zhel#|6@_h3#Con{pI{fAojj72j9nGa8}hLzy_U~j`ch}drcSj6fi^}ug9GW2BGpwg zvO3o{gcuyyZ`~ur^*HJUP_LZZlqPQ1vUq))ujxw}DgGJfOpHyy!x@m%vf>!9NYI;g zI>0XzS1J=wu&bdk8nP-(~mih!Y3jGwH?BQ=VY!KMA zt~s~B>iW!7OHVWTY|K$#Bf*{F^l5nPhq^gEhJ5nI^YXTJ#9qB-`EWqpj9=zem5Ip4 zwEHwdOPP}S_pMhvKDqFzwtl?5&^GS-OGY8u=>bE(R;u{Z5=L)A&yF84I5p(=Hi0c}F8*0ffTIL8j0crQc1_RM>>BH>zb(xtrdw)yV&cb^;zxV$Ti)E9dc8k; zFtg{hVdJUPM=X5KS8GJ5Sg0uqZ0t*yGREEr)(g(jdDwqgUO`>!MYGKh(NmHV`-7u0 zR<_w%w44){lUwurdBjr2W&Un?YY(Is6lq$Dt9svVxc7eSy%ZfqEM>DqTG|V%nKwgM zvnS;h>)2)tck0qmvg~CyZLYZ5a#+0n)X(w@j6!~DE7NpceV_23dHRYQ@2>p92xTc_ zz^^>=;G={zwO$A~nA0NYHLX-si%C@9c>i#IZi9~Wj|Tp5MAqo8jSUj*94jv_WnvZH zD#7FZ(#5No8JCIyDzDjvupQuA?Zt zV0K3gUzWEvmR+c=(M?5r`4OCi%@3Mi(o$lEXCEEt6>GHb+!k|erQ-3oebxt0r6(RK z(|4g25D2f{NX_PnAR>u36^{F0r2`C9oFsU<5P%P4So|31b7~k9+ zZ?LgL-gZ+y`!Y~nS7}|m%j3nk=j-mN$m7Ste0i+w3|EY5@^=+>Rn|Xv-KsD!Ll-8% zf2L_+-eb@^+FD=FZVjXP`b*ZOaa8lZPbe~30z5~K{!%q!dVTh0=1OK8cmoK?JWBcxGU9ZDJfnJQ6 zjb8#S8$#Ubedl*iTz7mt-tg|Y-tjOc<=lEpZc7tMT~ofj@ry3^&9|@L!tvw5 z{NZ-;vIZefB+s1jXq>Ur*XCD`uRWrscxQ!TbfF44n1^PT2wk?WQ$g%O9MgFE7E zY|3IF-{3r_LqqXdGP|xR<7Qz|r?l=Z_w==~?+>4KVr=t&8xd(%YHC?KH*Hsb|5{6z z0KM2U!~Q2X)@~!Rtc+wguPKaRJ`EWH^J)XmUDobGCghUar3tF z32H3pi*pVZN;?r?1FNX~)b^+t#Fb`M<+ytld(pdkWXpPM>4rxu9y;9CC@v$5^wZ*| z+jpaw3%jS=+G2-BS@{KbChGWwRdHHu+`jCl#&lGr>4(F9%O?VP3lWsR7MA9gXsYN) z?qNen25^m!vh3CCPuu4At2g;+ghC+mgSAJF`uVJv&JO*wx%?79H<0)vP)>QR?yyqW z)L?mDW!<@C8Y^R~J{zfrMU3Y5e2kNPTzk&$2aP}b!$X#PrwtU3IGulY>S?4h=eGoN zKC3;YMr}jkxr0pRBMZB~jGM(=S?1@W5-Ly|S+s=y-Cl{wzDkLM-J%tNXH%SpnJ?ab zd-TbUkPyjKCO@?t9Jmz9&lr*oCSjOil8Xm=RI9xL`)Y@sMs{kbN3I{4dXfKT$KV&W zC;>Kp?120agI(icEQf+`y;;bqk{JCS7*?6Nv3{(tfIpx5wccB=npK&_wYL?Jf4^|| zN0!8cLuW>!6--WjI+Q34^Q8rX8$S1LF*jBDxU-JHs4@^YF z!Vj|0U|$`4B^|CMsJrpb0f*JA_DzZ>{A`}G;ZRz?{mKB6uDf-~s&2GNuC$IWeZqA0 zFKIE?cRWd?p?#*4e^~XrnWD-O`S@CVF+=0Q4Enmw&h+59mQ?tiZvD~T9#hxRBagC= zio$ZWM7nn-ZQfPri^->0uN zN!Q>U0{72&&(sadqF}at#Ow5+mNjP=o&D?dFY-|SaiqKE#8I!T6y_-8| zC|zT8Ua#CedRLVVHTT?SzRt68ew&JycN-21v5>1_-)1*TyZQO4nFjq?=_b?rT@^lP zJGOi<{`dx#(>A@{x`TEvZzh>Go@sxo%pK>Oyo9aiom0mWxj=fOl8eYAkhxG^AO+$6 ze)K}2%X=J5S6dsBA273(cQX~Bf26~>n^pei&a;o$u`@@qN8e3H9%m7EH0+(p*~iS) z_jMq6fZlar3EWIlUi*1{x7i>uvvkp++1Ab~`TX2RQJy1Oud-YMKbZHZu(Vi*1hFN~z4D5K??7uI!+X)+&B4f%ZgmF~z;^oo*& zIqhi!S6a=hNQnc3<<092N~)uZjBiSe=e9ckQ2Cuq9k~B5P(tmKdC)_0!K-J7Hp(yK zE9|{@x}#^U(|k;tgU@Nts@n1X9RDRNysH(cAs|A=TH8P9o9rt>EhCdu?RUY`MazY@ z(o&g$QnQ_dJf5RZ#p$eO^tN?${M^q2w%~|HqyF2YYb^GdbnKS*Zbi$w#f6}J=^yns$ZDB1M6g>4-yyb?CBF4AtSxI({ zbvU<4@zGS*x;eX!-L_mY$qFIOXSW=#qjA`Tz5S_lw>4N*)nkeZ)>R6EY+MQKnO{M6=eMlQ|jyGCGsTg+b% z^s!{C@91>%QxPDOJl&HKtzVX>nKE>Zaq(YIz7==>A*Z66LSXP04+Xxg_SuBqv<&T8 zCe{&SARvXxOwv&aF`8>7GA9b}rHxiQD?-(qomMZ&$7%72lRx|nfB49V*utm2J$*A1 zSuagg4dy5Bk%jVkVc7*sRX3GI^Vf_t4!Am0cg85`s?!(a--lv@oj}DmXViEq!pLLB zc65AI=rRPu^N9VGgfCkrf3qURQh29k z>xsqYORwX$dcRDO47}kfwkxVRxl;d!u0H=6d{K<|=523v{Fe80>i1e$8NW72c{a`$ zPan)H^lXcktIFxjj=KfyEI$s;js?XrV={$@{4k>+l~I{J>H)fh48rT zk>&(lN<=}{G@SV}&3`d%!t?zc7Um9@EOtJ}!)tiM3seH7Wn(tntK0SR8)86dps*sH zV{d9k*I16Mo#cf>64jg&WoG0GB+=G!h-c_C{&y>tYW-4U9|YboQiGY)||ip5JJY*xqOL;gW3!@mXfY4jQiTs zb5X2+Z*rEGR97}C+VtOFWsAQfXE+kl#>(tuWZT;ryD={W(27wyFCD zlEWVjfk0ZSDVLvA;`SUa9FnOPz5a3jbI<)E+b~NLq%C5$V}85mUS_7R?PSz;7;8K* zmYm!gBh1^0zfG_rI5X3hTg{x1B7wywoBE3{ch1x`qdUywfj$D{P%KY>KqBpzWpmso zUoK;#Kd`KA(9(o!>nEEHWnpfXnm@1wff)K*?FbN!7&H@j-9bgFzh9E!^hJz}Befkp z)mvqPk%ge38q&wBzyf>KJbfxjDr*VS2N~pDlHqSdzhXecq+cz*r z3AkUIZ|3$9=J{+pLfgGYa(AsiLC0=XTZUDdG?Tbc@gX%)cGJDQABBmH2Pvd}wMsz! zT7H^FsG%v`JEfmdUL!8G?_d0A%qA;oLL>MJ! zSkK-3$cq=bxlt1uMa}w>jYjBK&FbrhYA4oS6FsA$bluv|CEklYIU`q3@A!@@#TPd5 z&qf|ENlSUx_3h^x)W0 zI9oe1_l(QoUH2{$B*HhxRrl7To_d^i9ZL|pfHub>=JX6rTCdAn>+rK=UcptK$ldu_hucY0{4>_Qu!N&6QpcmHK= zEQM_@UGf@FEuZNY<%PYyV%>DlJHDHYc9fpQIaoW9nDwJDz+KtRNLAs}j$+_*ojY#I=X^Z>1(& z)OS4bw^f>n?SE&w%Hk$XimWU4FOqqDF5pT4e%U3OK4*gW=lsP5aF!0^Nc*z(;8Cli z%t)&aAC})+mvniVAOFD5)2u@8QgoY#hktCJHybdkzlYRf9o=w~^SCn(+q|qspQYrjUc_X475iQkQNm{S7KL0Ep@mp=Q%)ofO6PO`9w|8MX@`gz9>uYxkxW7kar2 z?AzrQrn5)Gc7@DbZfrOqvGe+FT@eoU#FMT^HyBmk@{K_qGuMgG#qdg6mt@yA<4qzK z?P(ACS}E|0P!9;*lz#3zEk!#61*nm{uXKhZQ`eYwX697ix+@DhN=Q19Utb$O8uIPE zk#(@WrK^Wmu_oUC@^|K;c$0w<*;P4#XtQV>k0$6SIuaT?diq zt_EZem{V!iq1zibS1eC7VVzypr|@c<&{m0OL+L9}!UMi;Y3cCM3qEPGL$Ce0Aj`o| z*RKhQQpbH*eme7KF0YpyetP|Sx)fRTRCur5W(C3oOAoz3gCZOWEsYevAzeTr+scw$ zJ-8}gT1Yfg>F7MrDA!*rv{mn;iyYIctNpT~$mW2$`8i5tUGOZ5-K6M#qyJZ-?KWQ@ z?n9^p<}_Y+w0%2dwR6`YU7b4{4tV>l9qiOn;z8#A4y6ogqMcor7${Y zFt|3f-EcRb@|vL~PY)>1Sd7djm|TGKE%J}BO4aZW7n*6x z@2l%XxAfdRi;SGyBqFANhTV4tHk7BOOl4*C!5LR|z{q;BmGLk9x8lN&x2X-!NQVJv zO5p3hqwa=d=pbZg=R{6Kjeh_5F-1A6D;!f;^oK&!T6}XOs(LEhlT%XtPy5~0Ir-H; zm-eERt?i}VPaHfwcb_|V{udto_3Is>^by}gzj-voQq)!o={nx6xx}Pyv*n271Eq%M z9ITMt-PW%e6`w8zA+z$?HsOi3 zGLC24%!B6VwWVpq=OsGWc^yg6pa`XXI?EOMTHol+F~Gf+22YK4kJsVssZGiOxQ>cz z&uZT0B*%OI1K33Yc4vB%pOfS4Q19bPltYGE8-dr!YbM^DvE@E?KVu1 zr>4wz?o!qTEvq1znN*#VckVqs08k7M(C=Bm<>g+y$5a3nI=jY)oDKC0+DI3noj29y z8B8AV#?{A^RS(6hDQ213*{uP7vN7l&y~?pFh6o^&PshY{=w6giv*o6<6Fl*)KA+-L zoEBN=hi^|S^EV@RnfzJvg}-;;UfIExlE`~_^si4u838C|?%9g-&>dUAQ+YipLCEp) zmescf`THUV!4)|=3)q=G*NCfzNM~j^GB50vB5>2LXBqDcz3=;%^KkOZC@OZT{8d>v zulKtekrt%B?$*2M(Q#y?%AE85{#Om20$yADar$J8didL0ZnXKX^Zw>dg6ULZwe?j@ zY=w;4V;}nSP?%nH{u2lObqa}1Slau~(O-4-uUzp{(|VCrpa0PP+U=gX*x=AmlOgR{ z6*u*p$r?)6upj|nRdaiFw8!{w;h^sxz--jM+HSL}+qoWdY1lbvR?A28`t4n%xO@_I z|C#15R*wAC`dl&@2asjwqtJiA{~4f(=Ie!Z>zeU^>OL+kiEs;Gp`)Nr$t7v=QL9+9 zd7(Ab%>zBF&zhZjbt@t*15$? z2qp`!*O0|3g+#a2*Rx0kRx~Hsmi{+6$S1 zhPy>KtzZ=q;b>GMea9wl0lJVq?%$sAtR%g`*ttyhl(WLzR~do zRdSLW?X^Li1e{m4?xPjrh^=6j&PFI(ICz8eXIvaf0D=dk+)8aci_j6cXEH)kJYH?d-g96MwW?!;n@?(@HwK(7_zCXhY*m!KkHfh;;egR?O z+wh(?#7rKrnYi~|X@iE1u+CE~J89H(biQC#XFlePsT?^%flF~XoVdv|c6N4j1{FyL zazrw8!M5eDFtg9K(T-z;TimoTu@A4_;|aRKzt^jI4}bU`^eF&G(!V%HVe{Y~%s@S{ z^>Wt}{D%`R9#Xq8P6f1RB5QyZIre~(lG5?Sa}+Yj7B&YPIT&F=>JCZG7cVY}vGPNG z^~t`VJ%@sw2hZlOAl#Az5Mw7=pjvvJM2KW0v_y0L6rIT_AoT<6fY= zD}(I{u!|EX?n3+nK4fk~!zsoEIR0dToSg;r3mKNMO2nWI{Iqe^6f6Q%^nK}TzFmmhGK050%wFH^H2q>y26kTL~~j*QrJh2JKV62Oy# z_dhdkQb&iA3c8U9B%p-P2lEj+mxl&+cNzE`{9uKx6Q?H!p3r{BnfIukl*xg(CxN6W z{8=9Vd2iw4t{zXIzWUPC#0Mrh#Bg6B+{WIyT$e~km89p5M029At|9{>)OXAkPx$8l`;U9*~!lqa!jWs8}wHwSL(BH>ms46f3(P@B1b2Ue3L^zBnc!@`8zO4ThD9q<90# zs}clh%xe2A9O^*Rm07zT>b%VPesgFmAA<)WNAqR3Pls0drb zZ}VbE4UNmlhy|HoNv;CZ_aK^C^6`5AlPs~CDpnV$7yU5633Ps{wDj~y)3L4y--?9O zQkY;-0@fOh^wFct$a3I{9|B6*5Tw#aYF@<-!VRDLSMFW*qoD`ag2aiRKBlhjFB3|H z(}e$tC6Z#*%?8K!^NFU~;)P&L6TXf?OM^(OsW(X5xin_i=7;b6cIIbNt-?X@JL-x=iuv<(DgEql+^tNz_jhmMjGZmb%iC_w%WHN=P z;yU=C71j(y%L*OMzMitv$Nrzob_u1uy&WXHzy}ZL=cw86a#mxCgn00>V3mA46-*j# zgC-N6s2XZ&RG*=zdL&`H6Z>L4B!uXb1t2YP{F#2WnFdN2Jo#X-JmXJi`b0#m+k8J( z$4@O1)14l}RTWDf@bs31G@7m@#W+T(h8pb$-`b5|qMDnV!`7Ckw$`7AR^lAIR>_TE z&lzEnVViOx4r8zl7>6Sutd3$T1Q{=hBM>a04yba7*bvfFT**y$_a$JC2Hn3u++bNo z&R7GRW;$pvsHX5I*DI>)8(}Iz#5XE3DS?KDhN>rxVR(3$4C4Y#k-X_2$>wDs`p3L} z-G=Yk!}#TuJ1vIHAkAdzdMC~6FeF38Yu_i@%|(1zamVDl2~oPj8XAQ4TecK+;oZZ3 z^a-r`lrwR(mO@g`{6niu@p!sj5-l~hi8tn-AQaH|zmgJ$ZjX4Ox`0YPP@eDalm1KhA&9{RrLc2{Dk!C z?N~apdZlk>GJ~yGPZ{?<>Hl?{zdz_lMNBhuSfz^Llq%%+23*d)muvZxPD7SyTK&2i z?_2^CJ1a&=K!6Hh$&B<>*a1|e_E1}Zf_dP2pCN>=5<{PtfH+@6VRKrXzd@eNCd1d1 z-|pc=q|7B@^Tz6RPwQ|Aj2Wb5_2-GDhOGuAz|ZzF=%tJg?E zE@T4h*ou+HbbC#McoF)=flq@i?d`^I7M~pA1{DWVBjRBDrJ;ex1tai6kQebK>6PFR zpnA0XA{$nRe`ylt6+li_`HPE;M1f3@Tn9YGMWF@&^*qw(7*;t>eD?({1`6aRdoD=f z4NVOEG%e4VvD8481W8_#!zc?x1rVPwxRB6#SWd1PSB?mo;xsP~S~J zI8NM&$N0lAs8XIkJl<*Ri9`=fM^ox&WMo8|vm0}_{M_t{*$RO}70X`y82hIfg(W3} z!F0!Ps!czoTq-Iwq@~`6UwU-tm>Kk_m==bakazWJ7FUuSl8Ukcnc{z(u&c;OWKM`- zK)Ec0K$XJEY6Aa7JOyo~SH5@dd|c-@MAG4Lw!azdI(`tx6;!H9Z8N5k&XLZ>3o1b( zN#&B9d?W4lR~$7UchZF@%J_hKUV@wJp3?s2m;d#LjB!BQLJFdJCrdN7MvF zL|EZaid@yCJflYp+R_cA6v3K01rr2JW7&bcj0&@Hpy#0H5-pRQ3+rjZBXfOmDUlyhVBY3G%sgM$2$6a@dCWYJNmplnCc6I0uQHQ|M01`B(&F8D}j3Jz_5xMb!cW4<@H0#5!%;?|ox%R(9W_wIcF6ZCH|Ie-?i0EZLV z&Pc?<5H!djXJ}m1PnXzxeW%!GxJ8V98sc zH6+@Zj?&;QW;ft+{b1_}n6E5Bwol8zP&31@-lA@Hf z3+*LM)pLGuyYKJs`~Cf%=l?&R<9HnRaYXkuKA+F~{eG?UJYTQ+1T>Qvd+R&@A}hz$ zNKRHZ1`!a`M;1L!ykF*Vz>CkNQ9o|t$K~hRM7aIy&qCa3XZA%RTx|1-1sXMXe_1PcEa z&J$@RYntN!=n?{c!siJ;Q&6gH@qZNnk$*yC{m=;gH;Ep>z0Jnr|3_>0=K}uY`TYB< zZ^W=wUte^3>=1saR?IU{l&w%sXu`#)ARA6jPQhXk^~}d7H~v|@|A#Me&?F-)#C-AH zn1870x%%i0J~l=SVIyKN#@8C1Z%^@wZyw-M) zN$5qq64h%yzU~wh3xU1An;94|2FsrEEX8VzDl-LaOO;FkRHQv;r$Es^mG|iYaD&0F zkX;s(fuh|goAo(~=PtX}eXW~mQhw#~aLAG_toLrT^#XRV-3H)8N1xr)0NxEvUr0X!!Z*j0T*qIU z+)L^^nYGj}NKQE!8_n^g;xwaI(7?w)fYDHTY|K76RIgiHl&Qbn5G3qgXrW%#m(YWe zUS3^&O{W+#o~fC^viC=rfrlK)%io(Ic975m-Hr!wy`^0U0DJ0QpNW8Ci{&`x$Jn3K zu+mA>5MeOF3wX9Y$+onRW+AGBr_;k;$V77)wzB~s;-ZZ;I+ZYSYmW1P4Ag~IY>xoN zp>^EzeJ_cUW(lJ7sj*6>{((vq278f&T8KWg zXEN*`sY2362v^!fion|UU?lB)dK8S@+LbFgxhNKN6j|AFDY+=_@`xyvU4$KQq6I41I_sBq8gNpetc?)DXP0=MmQrm-dO(`trI=Sju zNypvHrkI&F{noeG(UKS_G>;!YCZodxVk>(WmoQ@){Ri855&s(>Ts7nR7Z*SV<9x3X zrPjmmuu@f3$iS8D65dFXoTFWv6 zIN%5)YyGRwE_jZKrXWRcDmq$U z9q(pP6i=T&@PA3lLeMU%^0i~On^Guu^_LLJl~a}rqT*XjRy}sz#u)f?roZz`H57Yz z)I{|%gG`uDxsJ>8Hw`v*|SpArcjgE=jPkvs=fqB(N7oJv&@Dvi2^(?$ zfdjNZ$Eu|}ZH7_62ooC`PxO!R94TUko}Ca7W6JFXNPk8Af^N;APC?9d{}sLyqak&4 z3hx$NBA#LR?^=8d4>e}{fz4fSk0gE?6erhHlq)DKtR0k2UcmGs3|%;lPl^lX*%z$u z>EwNJb&L-u@KA`M;Ef@5{k{YKdv*1;g$EPXC+!pq zFQmWMdTULoJRVcZv1=|mGl3OyT6x!ARGgEKRc4@`%Pn1(3jID>i>axpDIaUsxsIVC zzzZ;|kLGc`BPiRDyLH*dBgc4AA3oX=s#b?-FHMpn}564i??AVQ{C>h3U zSx^CXTi;Rh3Mkg&gpxDmlXiQej-Fj?Fr+uG2#M~!UM4FYC4P%=<7uU~{97epuR1)5%sBe&;A zO)W*)9*k{$)Nu3ySz=(B<~0IAJ=t%)zkY`l!NS09#fM}h)>nU`V})u{{gldU+mA3j zy}O}!z9g!3jUX)}z9X8y_?-IGIX2gjkrxdHAeh%|*{kxw&jE3KAbm1iP~Gp&`x!nk}_cxKqv8;9P=zR%53K%#PQZcdr zUsWPOx4N47TQ@zK&;|a?_JEY7C&B;e+jx_%bK+B7XjB!j&1iqn(zp{%< zyzJ$hyy_z!YFc8DoPZvIzyNE0m8=V%yf}GwV^R8Y|DYc~Gy=dkQf$y>zTms+h(44= zcpVlF;@*hoW`r8}zv@H|#xt-=)L(5@jOaHQu%neJ4)@8 z#hhByF+a$1x_ZZ!6!68re!H%$!N&VyM8tsvpDg#-%#HKWk;m17ThsHBLA9Ub)jgS> zV?pI9P!a`8!uiPUHeXHq)GCnm#_u=EWUXBjEro|%O(*>$>%T#@{xn3$UqUsF{=QmV z_Lhp^5tAb-jD%K-yi|SZ$C!h9JFT9Zx^dwsoe9|6eG51Ow3|+P_`H zi%vYz`gO@YLm1H{t%VI06tL;LK~Eyc)7UIiV5&O~z8Z-&88|juta{lz2 z^)92$FSfoINHZsZ`T1{d1P$NTm-$+h;yZw}X{*c0Jy0Zpj?_7rb@}Vps&)`Kdz#YN z0b1X;?iro1a2aV`?~+1DK0rO&RG9?rX}xd3`Lu|oC3k|8PklblD$Tcli9Im?6;(O4 zfbR*;xRub$zN>_EMf=N}2Nn&9zttd6^(ANYZ$|TrsjuvBL8uJe77KY+5O@T+BR`Iw zS-uOuBUtHw3ttT904weMo^dE_=tO^01aXC+eDzd7?D_^=TwBop&fA$ldYCY&0kH{L zQV=re6bIWb=q=qnF7R?7@nU^**Fy#X-El(rOSk^a1icW-B2u&PoKLeM;Z7ZDReH5R>7gq!{&D zN^|*j6sAT$2ZuopW}HX11EX)JrCf(;ofv%ap=(`}446eu-zO$PmF&htfhwYfnz_Iit&uzk5bRz9p7?&#N%d-$MY0 zG${4GpwXJQ<-fX2qax{$2^gfCKoRqMQ@O~N^sgN3p|5Ce#<`L<+pvybzQ?^ zFDI^l(X?CwCndmyoAgW6lCpQ!Qh+kjlz%q7Y~$|>PLae&Yy?8?u& zhI3M4GARx*iXoamtb5zlu=)}4kC?DKV89Uca@5jRZSU$IJZ+C^+rPuLC++@0t7I z;BilfXNNv~0N-A4TmsJ%Y+0ttneT&yCGK2{^p6LZj-gt72E1Y;sJE zagxd@myub`5fHHcD3%QbxwC?q(<`${JiY}}OP_@L?!UdOre z*yCu`-i;|5DvjHk61AwN;FPAl=>7PfMcPv1>N#VX)q6t|FX865`^`b0@Z)QmIjE?g zpX(8~f&IAAi6EOo`7d~)jNCELwM3daGAW$KVaVYodB!HQHl@w;B=3^-tzobjAZnlJ zsqv3oSYxwJX^Z=FW$d#(2<_CrveJ`)N5NzTfp;orXfAmq0L?;*3*1*-*<5#Ru@I9 zs@jKFk1K@~ZSi2&j zT2Ou@v0Z`o!~ufQCrxcvOV%D6w2&_Euyr{^4L_Fk%(Th5y6SM|!c+n+rE#L=VBjeZ z{>V*g$H{t*HfF2i);2JbtY>{Esx=ZH{#fXGr4*Yg5V8314#x`N%L=or1cQ@O&Kx)- zXwPmxFncLw_4d8@SL}JALOQTqLe9Wy`(&%hqOAPuneKUKeDANe;;(ScdF4|e7S_%c!;HwEKw-V-Q-)H~V;Dx4OnpzsqtTzVOxd=+U6mQa3+KCMkXW0d`J??p0N{CSK4{Yz3KkJ7V$q;_{2%2HuVj zDK2L!KcL3T%VorzvwixRi__VX)B4Fh8GCwa5;?`%UNfDR=U@9_mncPf?b=Rv&&C3I z7LF3~_jh}oF9+S+v8-c@uGT(DZe4S8W}-Fq`jesw8n@`j;usbCKdjW%8(_`#zH!d$ z#i1DeoV9+rW8_%hHtXFZ{l+@hDt)DmjVvMymaOFmdcIkC{8;BE*UHx|<^9#&S>gOx z#i*N7OPnWYVQ&;E}MnH=Ig;Wl7gB`L;~NZtc>#bY$S>IX4zmF?>kLWmCF>E_d*OW207`Jikr0W;D&H1`?-3As4 z?n5cL2JQCULw`p5V$k`r2Xlte;;Sa_v%XxsgIi<8_xg8?fVcvGh*9EVTM@VRo+yaWo`k*y+^z@tu(k`a1u1?J1OifE0 z1?)*ngIb^+hMoc!0-H{ZXlh6U5{iB4d0t{#EHWvH#UZpbXfb!p%`Fg!7~nS69Xoc= z6rz?@iy^*f!&mb0qY!X%8X6@fCA6CZuZCaACO-(h@z7?O2ED`pe?G9JCZod{9!n8k zy>jJBapQZr*|JQmc;yCsYl>@^A-e1aiwGqj{kRI9&v=1NVq8NBy8qMAfJ)QTR-la* z^ed5R3u?z$p6U$=g&vBYUaC@*lo+?WcpAB$T-`QD_#ug)MEy)%brUG6i4GoG3=i1K9awgdn1fvr- znjq2;Y>;#Q{8Y0`Ev)Tfav{hpG79x zZPWOnj8;-t$EReSE30~WtgrafClP1~^e`1*l$Zppvz%Ik-K60=FUl>h6?EYOYm|mp z0B*q+UC(L!(9$3C+2wdZf7ZppAD#JrB=^*JHo)^d7dGHOTS3FvP~6>aMGDc?9H)+k zhID}es56Zp+7ZS8I`^C&TYB8h%?=D}pzS4Z-f&{LiV=Jc9$hG=O-RbfFrgKHA($;A z-LDscd(8<3jnS!7M4xEl3z5ut1K+{Jce**(>9Yo{iSZRQ#3G?TB0Vu&YzJ*T$+-3!w<4)MZp#^zDt)biBKueQx@8FH{88MS1FfYZ?Y->Vhr_e zc~vfJiW>Xr$-$D3ZiljL$9qz`ctT!m`aG#$)KlB_3R9^mI78*-nY8e;cs22D*!q?x zxf~ayMZ%Yvv=7iH z!hoS0;0|QhjiQBb7xwPR=x7o8e`a=p=RmZPB@hGOodG1DJCg(3GC0u%?E( z{!nAGon`XLfyR}XC{LUNV)Tv&I@5FPO+U&SBkQSkvX76Z9b+sGn3!W8@Ot63N6&*J zLPk$co!%#$*jT0Dd=VWz33q@>E)9*7lat2j2@aZLQzLf9Htdum(Ny3JQF)$g$nYGdN9B=$_cDQW4LN=oBBW zZ(|cJzc5RVRXfk@2g}-pCaxGsuAXQ)t-7n`-8;E&m$P@TfPG(2Ol0rr=^;ZbhwnzP z+bp{{WjDv7MOTmO($T=K!K@2_pwZsv2J#{?*uVqo`K`w=T|rP-xD#wI%e$4Ik5@%& zivv?8p2#|np-Y5i2cu53o#0zyD#B%Kd3xC#o7|);fO6$j+sC5>6tTaRaSqK*`4n2 zsXQnIOk}Pvbj4R(ypq}|`oKn>*p8Ri+OxUbRkF%HpWJdf({_C3a{qxZ8M$5)H*czj zM?|n<nrnf^c&a-eQ{ISf%h+#UqyGt3JUt%~ef2f!yo4B|* z(FAgRJ~+5IkFd@r5<2OrsoiujS9Bv_q&%vDC4j}nN^_AMi+rV79q<+Cxe7FVjBlZ$ zq^V$Lr8(CBK>=7XFSh$DxPCsJV%V7W_I5vFZ^Gb1&(Wcy_d1t9FbJ}aScFK;qu@BZ z`eP)bCc)Vw2!7~Y$wuzLPNTWlnja>(G9X7wLnGIcN{xtk>64z30Wy(l#TLXL82M8M z`$rfWEG?}pC+T7(`ww;!F&LR3Im)uNuUWHZvwsk1hiDhgh-oNjn;L+v%}j!Sd387P z_HA~3{gUIl=<&A@iY>YiM!LMJDkvporC%Jc7^u)4)r&%25`#>LskMx`=+FsE2zFR0 z+RSIVeLk)prRBp$<2nCB9Z&J%>#NI3%F37!MlhLX|Js84Z5(Rj7*!uI&|rd%AGpN6 z$D@^YdHE#eYK@Ke?!bX)M=WNUMTB(t_s{J&+?+)ZMZ*fqOdf)FSRL_tKL{&D#5fuZ z&iZ95d16XE*m-WxaN~z(mbgDd$Bdh<9%Z*6;sF4J3=4(iqu;+7c^5O@Kh&&aT5fXI zEo@=QYsPz{tO7hF;b3aAvZ#jlZV5pJbpdS)OTM~%@k>K38R7u8SXe*3^jativ{b$o z5wXWvd2g%5U~1~lfus)_DcJ*WBNndjM4-;WuZhg^d~~0_pq4ty`TiSHx>D zRLx3}B5Uo^3Db<=vkZK_$}L^`CAGETB0eiI$p;F|y# zBdIf%p*K|ta6rgH-2d+DXA=_@+1S~Op07YW5=XV6W8Fl3$~@0N3hIAL%Zk_#QQ5v| zM3l4KVk^ESxHk@Is(H+7Oirwq>SBV8)5`C(7<4$RQgJHYXR4g{toq!?k9#P-9@+(d zI{ObriU}{I*s4gYaA5yszQsNY4|k;py5E@Rk)*fxQ1#t8TXo5;cT7Fqd_MQ_|FUI_ zge^;lEu+4Eu!XFfr&A0*I6*TZ(w3?!4uT-LSdW32m{g2T>WGopLKMbmNq%wUR-AXx z{}8?>7w-!Zr)OQZHL81Ye?RyRz~V=+3@S%HaJ06zdVcTYHw+bJRaI3@J^Y>t?qNM_ z!#S|&SHUYm*FeNimxb39gGUZl4-?H4SWNg!Fvzg`fKw~0s=h+&+)WxC+N zTgBDZueS&1@xYKIlDAq=ZZdOn>``Kvn*JJH`*4d0lBCS+>^PA7fJy2gw4`3Wx8I8d%APy7@l zAh+d~mzQ_;xqvE!9j@rHMFu1-4*n2_#3UytSDhKnub^jzZ7U1Yo@c=U%*@O@c4XI1 zK56gAL_&miwY5ly@Kx9M9!<#%i!eV(10t9LEcvU?x^fZ4@82(i6Lo0I*JNIPOVy=% zE@w9b=^ELL`vwlrF+9)wlU- ztOV)fWYnpdrnxyA3sJME#f{wMqAqUL106Rg1 zC1FQ$q>LmBl{vCc4DRYkx5#vm5DeCJ=&9b`#+vYtT}-!eE)`?H?r39%F}jv*o*XxgE&psAqny@hST zpZLOzG2g!u!^(so;sWB2l&#uN95ue_Wsc5KhM(SyjKagaesaP)g6%NItc@+Bf;R9L6*M%u3-_yG+?LZ$TBL~ z@>|NBRJ0Zn-p)mxS#J^giBe?UN6!b<)g8H^<_wOr@-&aLWP?LIiQSvjsjT_BtL0RwV>nncuj)Y20N20wTlji9O=+~scMsAZ! z63EvLSk*L?RIt{K?9U$4c9}9VAy!oH1{Qw}&L-S-VSPR0p=!Roj<>JFZTsqMZ1?Ts zz+u2;@1`ZjPB5jXr`LS=V2F26Z=K>?A^&-%6vv{Y-8Ss;$g#V10>(;GPUbE)qvprg7)=HnJkI=Akl*HH+`P3gLy6#-MaFe{PK4P3cpozf9dT%|YzY?T2tH`Cv$K0Joo4rh4K*eyDGbrHgw}lS z_6i%`MHKSY&9`;v{24HS#QJCT+3HJ-18>t?^#J@(nv}xA#S2p?n>W9Fb6axg+oOP> zCF?JKNq&2GRvuqYDd!q!6Xy!HQ}!B@#>y<)YUoOqSl-JfP$(l~-AUJsSyryJ$mCCw zBQD10nrQ7eqhlS;Xn7RC0Q;7Fw07+!+#Y7?^t^cVXqn2IH4>x;fd?KSIuKzd5r#I{ zjz`Fg(G;H*eq3Xy)uJva?g_X214;zoQbN$Fy3%I%? zX7sGXIV-48HXWuOn>kVIOdD^$fTO}ADq5OXJAqw&Jwi{8{~l6cVvB`}U2bm;j!eZx z2(%;nNT{e>SQ$^Ec(%PHcVx7EIsQsgk}HCa6??2ZA1>w`rtR$B#RfYIGkL7-&CS)f z>Cy7dGvec!*~xQmKAqh;-??_y{*Ik=$o?B7pG4xsut>8sKII}2_8r$U>wLf)dqGA< zrlzhgXv38`U23wuQF8J5AXF`mOCfRVEK=q`t*_`o#I+Ors?PT#gxa4zH4~B!Iyb!S z##5~O3X60;j*?^V4YU|Y@KUi)p1f8FZA`MU;gaSMtcmWYUty8zW54~K+4+}y9?S&E z-TM3VQl!6=u-X7Q^inI|_hY~H-t!NH-hGF}rRK=vg| zmN+;%O0HY?wCa>1D+yyuccz-{k5nX%Jf>w9`3JSPld-U|`sZxB%DxkIM9eXL{(KcH zq6NA&e=n{-*0*pMddOduDyoR6L?zV^kC7Oa1b^H0<&7^3u>t4jKeZ1GaG(xIOf#}; z2o4JJJkit6h|7S{l;tQ|MlB5M>Z~SI5JdD88p;Jh9FJK3qh{jr5^c7=^rM{vAi@p% z${{)>e%49^+)QG4p2LwN_pEZ#)lN?s-}KSr-$M2`R&=6cy-3!*63($Xotb};pL(lV-!WVoC>(1UAJ&b0e>EcoNcuQR!b!jlef`~LR_jT|P&j`V8& zSanzw??%!6Vv5wcd-#o7Z<^oL&qmw zkDn;SU-s4bonf2gZcS(HGUWWKwV>g;!do?Qx#}dyg$MP>L@z#xJwmB2nu9QgtJMGd@yEW(;%hj|2eK@w}p> zn^CcsQN<|Pn~8*2s+ z_0%O^#wNu{~;M(6rmZ!bE@0bsi33)k~W0#op623=;%M_1G+4 zhAp~PAHYNEm_z6;u{W3@E4o?W$HfEeUymQ2OF1fW=JuL|=3LzDuccf{si*fP;5mN= zI{*#Jf9->4CQ`M5_}P#bi+-$4GQj6~rX3-!#m_R>&?L06qd;hG#+$kP!0s$h%thQ= zY$YGLadf8)WA~~Xf+7(&BmELYl>6p9(XM#2p0LV0_wfGhLVXWYlfe)^ z8!<6G4w@z0MyTk8$76*f_F}6Ks$f`1(X3B_3I!1#S4Jgc+lLPyf+NjmxuO_yGv zpm5DZOFN(2a`m?n(8B18o7y5;JCL^_kE4Yr__NZ6;`~X`v%5MXe2X<94Vo4Ode=w@tlZ2 z(|q^_NuQO(!<$lcGfia~{3@zzntDER78U!%X(m<_WQ_05up1j+!W1z-34JDczaX?u zCE-+5vQh7zsw<9l$gYO&}8Ihe7zOKMY;u32PL;8SFOa$Q! z5{i11+TXoTf}0=jt7k{(?iUp$2J9*L{{6V)3y1;mS%is&)e%-n*3~I=CyKU&2W-t1 z!XPh>>k*w`Y@${r%L$z=Kw}2PwfR0BKv=>(guvuXL!PZiYB*Eef2K%G+Mv|^# z8(CuQgbJ)P9Ri3;qP?7`s&h$8FSRm25*ZU8&kmbRgfuJU4KkctOcVC5vCO%SFWdK} zN(di~(v9Netw~+IgI~5lw#9@Zw6byyG+dh08=kByEL3jcj;i4CF_n;zx~DlIrib^B z-<44FID9px4J+H~_+r`!FY*Gs^A*BBJTJH$=z>L2abc&Y_kr3GU>ylIIw~r`*1_RC zn68DTrGogfk;zGYxTwTI#DHa&`w7c$Q_es#@tN@XFrUx&US_MQV=I13xnFTnCxBGy z(f&3Ap0vR`FbHLG1nn42OZ&fxeRi>lh=@QZ2+#$>T5)_6#F<*2TQ*XOd&G)ER#<#x zX-F*ws4KSYqvG*0asA*XFdqa`kfbwG;aDyKO6pjcA6~%!hKN4EIxnBTL>U@z(uTMb zsBXpq;6QXDBq};!2X~s9dPI~`Q(KG5^gSYW#qwix5LsbR7-Pi{ioIog^^}DqedVZ% zgDpK=RMj1I`YpHkB4P!l36yDO#=zg}k;#(S*jRsL%=ql@SS?hum?}5bW`EDXqdvF$ zj@i(f;lLo&kp1lhp_1izad0{YVU1<>OL7syW7O(`@7`6wa-BHA$4mmFfsh1dW@X{! z(Cyk62aOVr5QopXg=F_(g?OR@iaE@|cIYJFrcOKuo#`PmbP#%RKD5y_j=J?&YW9}jx?PzKKlTL9ot z7u^heJ{E6-p_u-@&ZXl+E$koxtQ;kwKMI0Z3?6)&S8GB!ztFyBscvxq<2S@YYmdsF zvKJNi6+;N@F>EbL!6dTov5yG*5oG8|zW$<9TogEqy@%xc9?oicMMdfN{wJb-7#h7= zwNw+I4&<;r?|~u{ME94ND>u20J1}4~pix5M^Wqy#$-cg70uC?!5y|4g0g4RACNT=- zi*Olf*QIPN;M%ff3lSYStGq7sUzl#k3NT>(>XK}tD>pC9Yx~&=mX?(rBiprPr@;wf0>d-I}_(9FWS_3bW1g`k%l z#s{oBF+x|OjOc2^4w3TxOK<8Fe+{ENX8Yrmh$!y13)S|(0Y+pML*MSGzfh-|ir&c^ zo|`SnDx3||K64JSEyyPPblJ}nm$Q;k$^WJ}qVeW|6UZM}SyvR?80acDt#qgf!zNq4 ze4{Ake8_s{>k-8$oZyh+DzHIZ$xtiWR901D^@(pdTK4iTP5{pN9a|l}Q3sMuTLNB* zJ)*iQCex=i1K-+U0+2xT5{ed6J!??d>#n=nnG5~2AeMW@(Phjele-tOqg|)RUV7XU z2zDc*>%)xyfd`nlhnwD=H^ZI$?3#!dHbfXAzKDtt>{Xl@&r|8!9iqk0^ccZ~?0Wty z52N%g{mr*%BNw%_EV*^-Rx{5CN+!n%ddGaTtY0Ttb9t{^Y3B-TL)=!+y9cW=^z_1> z7v6=>-*H>-AW8ErK214*{XZV$M$2K z`Xo|O9D<&!IsvhDNhH9eL zsW-Dyq)mz9D`7Wb6aY06&p&k|no3|ZV)f)1NT`F<;E-DiHzYlhT)LEXh5Q^k6P)PX zN5{Hm_EnXW_~iW36}>-io1eleQ4yO#IW+*CJRRX|TE7@wr}1SXfxi zYgOMpa_m%dvho>^r1nM9(3dd*pz7)E#R&6{4ja{O)$dn_!vkbV^x6%k)|K1|z2 zf(m>=4X1tI{130)=|`6niw842D@&1$x>AcQ(K71_1v_G159#4wi5H7$XAa}}EpyEH zk=q=GQcT{#!Ba;^*}LDwH#q5Se(MsOU?p;L%bWI(*_>E``!)~lLu|%6j*`N!4PK3%0_UHsJ{X(7 zg&v}J`iBn>3a;{INS|Wd_a;~Y$7;*1wdZz72{;@+9J}$wm)$Q$m2eFk8pI6P;U`DW0FBb6Z@ROJ64^rv_cWzPKs% zNLbUlb;*ZqL-66dpO0~?9G_-FQ=Fji@Rc(Y3t`1+p`3SN{0%SPEnG5Nvu2IshYeB! z=D6I_i}n4r*>?rTzjMYUByeVwZAtzyxrVrg(7QQ{8OMBl{i5=*S9igq(R6%x(kCKga(1XTZP9hbskDj(#a(BKSVHE{Y&ZIE!u5ZA&-+KlrFVpOp!GlBr6uZ?9qaK-XML!q10SLkQcjjQ z>X?xIx$$I()VXdmk%i>b`{qC30s!Q%tm5vLFs9u|5X6#kM>i7XsHVsfk#$IDE*D!a zU~dPCBNEumVOHsDgJU|ySEgU63j#d)HupfM_+IV`*lHuMJ;+#>88L%Z7rp)KwSif{ zvb)t9m?zvhV;($UWL|GUBvnb`ZPStmerE7>+mOXFkQk04YecomE*m|sKR%WkacI3Z zJJ8+ZSFV3L^DP6Z!W|fpN0;z;UoAg1vcNDvj_Di&Nz@c4{%rn&7KNbTopSFF00q-i z?yBTWBW@mka(lx~ z2V46$39j2n(xT0I^qflu`3?JZ6!o1C$^~5&0ACOo-Cs$>>#r0d0fkye84U0TfxuiY0MJ}P((e=@1vCu{VtiajzA5}-j z?8N#kEIJAk$=GcB-eqpv93 zJ#uS%CAJ@GeK^trKT8Vh3Ki8!L-_|fVn6g>p5e+GSxL!Y2 zVJ_pD0rEig=n$=n(_()NBJC#c44RZD3p(Bzevz|JR#Vg#@za9Yl9{b0uO_L%w`1Am zR~KJ?%XAA9Z+_z+RQWZoqc(TBaO3WKZ#`P<_B*iP`t|5D;_z%Ls!GNT+*Ue`F<4$$ z&xiwKeTIhTciGB}mXv#rGZ;kG2cblziHWm^xg2NiD+(sIg2C+4oSfseIfu+C2Ctjd z>1V#{f}+h&SB>y*-&j!XFN0L%&)(E9m(P))Y0(K{X~X2BEm5YS-pI62(7esd;`|M8Z>Y$C1_kLB6`h zw*$OYTs=zq_(RmFJYEjmI2|KXCh5gKL;KtO;C%gqCjYfcQZFQ(>vM(MT;R}@vt^(v zm7QaPB)+pCpxqiTN*nbBpGUhw7? zVTy-bT)_%|W|Jq1Q{?$p>L;8L`q$NRkz-expdz^XkxA!6CF7fF6E8gW5=;1SOH`o+ z_Wr*(=V#7jU6wzl=r^o+Z4iqBMGhNLzAST26x2S&5LNK$U_zXt_bF~9yg`@K_&e<- zy(;nDzsrKLN8zMHAsdMLbGnwEA6~_55K?odnwP;KgrIu_DGldzo0GxS27Y=)3N0_Q0n8^XK(8>Z&`4IuzkVxuaCEUyoYnJbgF)i9eS!*5Bq~D-7tb z5berdzo1C&Z?+CGVQ}#)eXB1Wf7?YbeXTj5Z^w_gE9`1|CTL=hdbGwOO3aD)b#qS@ zmD;BF=I!I%dpbQR{rcjl?#&gN!*068&(FH0K!XtNJr6TKGO=-f+1<^5TWdOL(T@Tb zQ2-vD?9e=x{K9O?x!P#!@Lybjmf=d@2dl95IBGmNW~SL@ch8tbM!xlK$#!S`UiD@KjbQYR)17Mg_ zQh3F>52BJTb7{2f*O+jX@Ba(QP#H0N2s@N zvT<^9o|U+fciP>cCVh+F51XHIp7HS$qXSx%cfa=7p1U_&LOcxbw)j_v7HG)-;Xx=w zsotJFeJ3)430_>ID(CX-z?QnFW;;5D4}IktRL4l>n-_~8O^ys`h1~tL5P$_EE77sO zoD8%Kjvswwit;{HJ|L(gka^#9TvoGJPC!QNo<$xy*62yUXrKH}I2B;8Kdlou4hxuG zdD_GI911kQBf_S%v@G$(ga;p4PlNkgXg2B6(aV=!(l*&giyb-Q*dUC9Axc3A8b>Wt zx>{!Uu{chADPUKuF1VcSeVxzR&yScA#$DT|>{{@?-_RE#X4#gfCIwF`D-Y%0xhonQ zA73)(lejBEI3V4r7`mQhuSt*WMtdVJJl~jMCV2RwmmgWR$EQtbrH}5JWcGMdlKZKq zZpF-POOiAJ*35j^#pZiE=j0Fzv80+$9_xMnl|~>W{pkk^+^IDg9^CNJB|c{mm;#BQ z@|>pA-YOcOJoT=2&Q6pfsOqRzc3HSuAy^Gk89qa4jfjo>?-~(f=*%lo$|>2Vj+(*u z*;mSz3)s)~eZHY#H~udR=e?vie|fpmdp+6720GGq#j@h6x}A2X+RC)eC!s=LhtXCexnmF8H#!TI3R;!$3G-%^xuU1j9xz8rj*=`|<`LuZgAW|dXT zL`ZS~6k;E5c+CpsRxvK>_z~%$!s(hTyKIrl#;qT0Hf-Cl{h5;-Ns~)dk?p77-{G@2AZ=IY{bWwfxlN zz{Tigp^V)IWV`D1<;fmj-7|f=0&<_J5E(>+iS=#`m$l@dc^V~J?sqP2vzo#JAXMSj zn&l^dAcv@1ty=PF+4Sxgj;t9+N^ zl4^miDTZUo^84oAbii1&|MF$u#L{^yUEr%B71w8v&=M9)?aVQcCtYp1^dm4BCmb4> z%E}CTdg_0LIBOB&EXFU6EtE0RTBybxwpXpXF}A48le;8nWvfHy5Yr-isUM96N`w-x z+aN^#_U)UUZ}~$Axb(Wh9d`N6>_k992>;-(s6WdL_qPJY{;%OnTLT9vKgezMj;z=# zW<;2V>=rJISX#IiSo5tNjfjOYuo3P6xZKm9%R91E10G5Bjp@yQL|H_VZa|LRhhG)R zB%RbF>zaasfAapP(v~fD(n8&<2v#hU!V3^@yDGnPiPH8u<+Ip@D(QyodF zsjfD>u%t$6#Wz}Ye}-5j`XfJ<>gtWUzlc5}!8mRfF_gCX5V0;$uo1~GRv|ehz4)_R z?TFag-FB+tX7U+w3XyqRx*NbGdK zG;PVgG$yk0{>Z1?#Z-ZW+=cg<+wh@+FnK#Rd9`W1HtL*q5N#C+2x?r^EIKW))Sb=r z<;kk@<)%Pcb$>S`vIi8a;~l>zJf+Qf`en1w zpXdIMsswRIKyeLm5b;0)9vEm6nLj*(=V?V2Mrl-ZBhtC2uAQ?BWU?(UoPXfXJ!Q$xD80LGHST~m*5882GRaDNb7Dd-ycSS$ zf#o=VWf6e}`{egS3fL$h%mV%4fBH_+{XTzf?8{rfpXt0P6}d)|Ev{FYgZ?gL1NPn; z0zq`7bm!fiP6`S={kpp=AAhzR(wnNdY?eI>H4Dc;*B-mx(8 z=|7k5Pc`v(`29nj+_BC2gVCE;Zo0B$q>p1udr`W90TuhwV%Ry|!`7gG;N2j$A-vS9 zZ_HT^wa-`sGMtFyPFZ!e+4Rh=dOs_(Zx1;Bz0~*DX$yP8iHK6!WrD*vN;&)5v?sAz zBWb4%4phjS6tF?Vi6e8Y_b4B#1cZPWPJa9U$>(xYYvPT}#|J(zT6~zJMLfb+_46+L zlUn4TuJc<;jcNLyZoP_`@aO^9F9nUn%@c2vyW(uG+7Pz4zTd)(-rej?|69VB_KB^PvbjwyqtPpFjk?{99;=Wa<8v6%az&U`kAtf*zO^2Vm~Mo|1U zrfbp=g!?Z8d)U^>cZB%^Cer663GeEg`bAmg6i}4$K7QM*4s6(t=IL+I)?8~2TWIMN z!^c&u5r4~wVT<0w*4u85Re!&E5tRrtM>zwthK#37FjPthZu}K<{uCq1vylL9fes2f zCkJ>`;R@Boy^VtSg%0r1fOlxEJT&u5ekCrsZQbr&7him>mXuz>arp2PDy0-mKLg6{ zMx#KJlWc3X>!WcloVayPvDTD4-!7_koZ(YCv#ehbciNr<#^l(e-)Kn{KGlP*MU_4~ zmq_#7CRTZeHJtUfobwg8B>z9w572Q#X%5Hs6O8b3QpAg2o-T4 z(|Fep*ivw)Dk}f%+#S|?rF4D9{&5hAE#YpUTRb!}vYqzptfJ7hFWr=5OMBU%%9L1q z$d&f+gTRApSl7z$TqaJ-8o5YGN_wEJm(x7aQn$4Bo|@;>v76+MxwZCtmIwS%z=`tN zq60wkOAird4wF}9wjQ2p4K9$*h28I#CKmLI0&ki^B{v0?W4=YNPT8XrMhNQl(ihI6 zqRYFur}FPBwrEA6S}j|f1Il~^n_V08H}L$s17MIHZJ{fDI8Tos);T$>!ra)2Z?@Lu2&ORGEKu=C&hR zFLA5*1&?4ZnCR|VQlwUw)z>2kfuf#X2@h#;F!m^TLRUi659TtHaQ1l!Qubi$x6$K| zH-3U-fhao9M5v)!3lECOyA$-j8y^x%{$7|Jt)bRVyu4z#)#hv#qsem}B73De;w%5f z8ve$OaF*jt3?M(H@_w2a|HL+}(q29iIk^y>pKaU2# zbaU=XP{PMR#3p_|h89Q>j%HRJNNY2brGxFm%|UYxjH12FIO7q{y38pQqxhlzxAWM*`U;I3o+ zub)14G2>^A6FIj(VSq7$2`IYhc1T-H#e7@x&6{3}pj~0ETs&y$s<7tltRl9Pvybik zLgY9&$(`;WU$}@XTII7`pMz%R`?~SpSaRFW^TDl$}I38`SYLDXBinf2~nYLchIlnt~PdTP6Y*i3-1hFNnX%O8huB|P+vT~;%&x22pat% zW#pR5@s)iL#Ys>^noU zjSv+{_AR@TeP4&c%zM6Co}POCzxVxL@Bh8t=el~juIc+d-}~I>UOxAIpL3r!V^6sY zm6E3e4m%^j(E=b=?)>s#rv^BhfzO>mUoFT6k3x#k?l4<48O4u1iX1iGy-}3)4y4WK zUs*7Kjj2{!?zh|lpQh-XoNyCtnA{6c6ewm_RyHsJGry0ME6vT_)6xZmlWzk9?f@nb z`B=_?a88Qv&AYQ)AXTleVYBVu_(4*a^3KqD?pJ|HQjx7l&Jy?cU6JfQtaTwzd@K?WH%6d-T{ef zXn(92aE>4doCg}>YX}15cFWG45btggI{BWz1BvlUkf}RxV9fCATqVfs>1QZu6#M~Z zw!Gvo147k1C_)5@DbP2*x9umnUu-`-1GNIOZr%Y<7)f{-2(Ny4Bd%yQh8vuP?TB&p;jJ@<^j!LBx)Ujx%ci zf+l~>K?4vBGJMO40B(VJMb)PH&0)-!Tu`ai4G7e@AOq;6 zt~$Si8Klk~%EPb!$S`ef0RNHeTYJ)dG)m!fku&enQ{6ikQmqpQ#c!V`mprcqmKL<^ zEg4J9O*7dE<^f_M_?bukssNBcWeRO=?N!6Bs#BW%15v!PAf*bwy? zvoiiffB#H(LMI}Aq@djH`heQ#>!NIJ5a5#BI$7TAqI;_{bP>w2gB1agsJMh+kkhCE z3W44w66(-KzI-(~kO!MT^9UgP%^Snh{mC2-t5z4K%eBUbnao_!jsNBdevwlkh`Sfw zIgyVt11)4mR)}isN8t9Te2{rVkx=a|!9R z6BNuGYEMkCV8)8Q_x1DhLxYNK+iJq_a1WybE`*RXv9OyLvfw@xtJzmWiD?WJmIktn z^o3-qeU&a+(&CKt+tgGhTG8r;w?yAs$P;c>1xy-#NB((z!3M>!X|T=SQOR4B6#&bEOr81>|1x}_d^_!Ol>=3|!|b!>F+ zkT8dpMaNOj2qHhK-XTU@9?jt^Bnm`ExkQo6+S zQdg0li0L{wXV0S8n;4wQVYXpHm)SCoM50h%i#wKAm`pI_&V!nAJa;xGr`zDA>i=Q9 z&tY^IBi0?S8$XsjTKp`3x^wDmOrh+jYgN6<2yVl zi?7^7(xW8}U+|SK-6$wsG0WEG?|eMu-714NQpPPMXHU9+X_m8Fw_w?y?;fV_v~uG4 zwTkjs&yiWteHA_P_#$<#DP@*lI6V&Yt!RBN{FFCT**^|!TkovH0FB}dV zyhsg~l0U75_U;1fpMMVdc`E;KWWMyh{QU?-!r94AqNMRV>&myGbHl}S7T0DAFj!~Z z=VX`j_Ej@&)?G;CHdrq;avL>#Ct#co+S|F~7)r}MtTICYiF$i`AW5)}`Q+>zE#3Wm z_3#%~r4Ckoam0&zg>JasDnDmUW=N7%T{*R)U-^0Nd3#+4d3hL22rZwxEmdU$4j;MnjTPGV z`4@&$YNNt3mWrRBY*5ZyDjr-4y#p@cSuYZqm3jp!(eSPV^xOOT7aPx?8oWb4_50GC z8(a#M{D)vpkZ&zdZpJB}U)FdIlv)&Ahtys=>VBz@&7#)2(kE1PWdOQmYP!eOz0~~W zT580_^8XE$^a$25Ydo2}ij4p(6#gs)Q0p=gDD9G4QxViibItw_CjX)L@oI^-_G1P&N@{kC1&UH$E%J!)35%oJGBGNSj+0-Abe_j4 zv!$i>>5J2wOZU{SPW&O8e=wkm(?Z*PXd=KS#DZ*^%vY8a{7v;w)wnz9-YY0OC9(c; z2y2z9{OP$U-+2`3yIF1TIl@Grtt>Ob&BNaPc$ofI9aIFG+#pgK&q|qum7TerOFnn& zZiD#f5v6BK$=?PS8Sv|z7zxt6J=<%_U_Z`j(UxRwSTP!Fs~k*Ox_Mgv&(ASW@%rf&EEz{)K##1>LQ!-DVt&N-(&-ZX=G(J}Qj5W0oqsup(l75`~ zLZ@zf#~+64Z!=?JQPFKM48~WYh8y*jPMbh8vM*j!fmV0vZDL5Y zLXwiX#lVsD2djN%ZDYDMziSK(?Opl9@APuSw{d~miw6~D{nR!3W|_9ml$wB>9Aw$pUYF+V&4!7-d- zTE%79mO8q5Xs!CRbL-F%%5Rpma%YUhJlC(1^J6t#=1@5-uIN0;oO&EA+Nwx zFVnv2y|`SRTS*{&D0FcjyG&S_;1XxzTmo*>Ag9Rr)>+=z^=u$A z7i`O~%F72hkO|5klv-o64?`%R#_AVW*3gVK`LP$^-vGn_M?9avbu?Sna;#m80M`dTm5{`e{}F%$jYf@R+hcga4vH3RA?v+X6X}Z@Ru@fmEot_qEJuD z`Xx8wV@H&qjSda{CSNlAn6ayMIyx^P#%;9izeFSwf#C=~h;v&;7JFFl42{cPxS4 zK1sVm0$trwg8#+ghK0U%xmWgIdcuGEw87Ccwb!%@IHkc~kvt(kVHOwCYk8;ZW@W#A zYxRGD#Qtv{83+%}-Hft#3jYQ-{>z~Td_wn40raM~+NWFp{A?X{#l{4;`*J%*n$16K zV<8j2Gz_1ME;b!_$VPdp4{owclnZY2QK=Sgi9bcr!N1zD>gikuM~*w1j%EpI^&HMx z-Zg3O&^EE3^i*M9Ez5u+vhlCzEjbrjrJ zNl*b;k7_rva%WcTst)beJI(F@_8wJ*P>+|evz){>opuY#WRnmO)&h0l4Fy9U)lCTaOnN-{-1Oi;e7zg?_rK{j12#K>5 z`&-5Z%Y+>QQ76uvQ3L&l+QD&$HgKpPbosvq8mzxC)GGW8Aqa?D-)BA{hJr5nK6KjP zZaIbQs!)S_369%c`D^E%CeRmV(&g5lZwR2kU6Lvx5~N!tH5unkS%mMzfulnjQE&2T zGr1=I38H`f63^C`7}>ow@)agQ40B#c3(3N|S@O7bXZ=iTrHJNJf*z*#a9xYeX?(*yLP zDe%~I$@JgtkNib|f`*aM-H;vN9J{C z2R9ZjO=RLVwTv}1!a)Nci)Gbd&SN5%GYe72kBQVlzy%W1J}yX1-z|iKlXU8w$Bsq1 zX5E%|vS``qYv**$rX*HOt6Qhz$NXERsNlZih1f@P(9~^eN=2t1kVxP@ zp3TN_uwv{S9Nc;-;~12%`xljO+J%F(D_pcht_qy>suS1j5=Ut3J7iOg?}Ix8L8Gn+ z(4L1|4@_3Ww}R?AV#~z;V}hrh<_y5;hKydr2wxv_(f-Mbyc;)fCV}(4YQy|;4-Uj* z^qt>L#CQ8`#m$h;5)~kw%@czfSFAk2&{6T5PPb=9owMw{^={7FyPaVQI6wt5Xnuu6Wp34_2!EZL<+xY2Wp}D55$7m0n zj%&fS+qHn}VX7-&&ej$SEbSSA*lT6d2NE1q(DA^&(k8ymQl^LX%qGiY%0@D*?rwwSBR zI^dr{(l6}fM1{Wqs$u_XR?SzyA~nB31Pg>l^VY32&^sy-T7)slc_$tKUg*p~zjmE` z(9J{kt=VeFC#yD-PqJl#E6?IWcF?Q9Od1O}^VwiA2RKS-}414XW7dIgB{~{ zGpn6yYP^Dvn5-B8#RAVTKo4_AXMl0I{`pV zW~<$|!wlGgmcU?lwtvf<3Kl3J##Gm~s@mtBtKL`Nu z=YNz7zhn5{H1^Zob`Wb@|EgPFp-=NiO}jsq6n2?^+V2ZItp1ynPXzs&xTEaRU)5(7 z+glUMPY|{6*Td-8@LU+D8Son|=?Q%^4=5@zPnA%^4qSTw@~Q3cJn6RW9Oc}}q;)D< z^1wx%&ihjgva(^lxLEv7I^Xo+xD$tpj%%~xRb;9zKkVH4-w@M^-GG1*E%Fob@&~^@WnUa@Z(ltS^!)*+uiydQrD3h$fPzO5_`Q;f ztjrc#$(%e(*7Tu*J{N6hG|LSQQMmOX;Jk|0sy*zTs|PYNMWLD8dN|yV5!U2L*zxUw zjZ9k0vOKI_?0nHl)Ez$>`sO2btodM?3E)~P0NVk0-5SBZ+HV?C{N*~=*>#tda;9eYiv>8H2w z`i;7Jral)#Nrr}1Tfe%#9NjW~7Ey3w{sP!08 z!YwDF9_xY7O2IeMb>vTf=BE%^e9n?%KHG#O*0?2+L#UYOFHQG& z##P@j0)az9^A^``)4*Br2cygFzeWwYDvw* zyvSQ`513hUp18Zt$i;hsZsgos$RhM0crkiW_z@HUPwJi&6C=7gz z!DJhii@N#fJB?RtA-}^=GEqHO0CnU&ujVtya!hNkdyN?NIo880OMjh77(cfudeP!y zPK#IX7qd-b*HHPc0*9f<-N?8pafGt?vOu3RQCu`sU1P>s$8n~0X0w(tvc!1v6oHbK zd1Pw>_8dTZV!Vejs5xo2W(o5JuLK~)LH*BEk*M%8cH|jxIY>A^&12WtE{lw{v-Rkw zo8~iVXpO9hTbp(BghN>N);%9kgxL)!zL#CQD@ezxw5-Z7qL1TqGV;xbzT5M-#_~8m zz1b`QltzIrgj5rjfsDL90pefUjtOprm_DxhA>~q$-PrMv&eW&2%;OQA{VkVe{H!m4 zhi`;#E#6Vo(L=YDBvhA*DbkxS=R3_LDa2KQzV)c}@JD)DM)F!^mieqx?f(Bl&?xpu z)@Q5WjFm+WA=&X2KF2ZeC(Wo%ldzDHxjfXZG#tAK2~*=Bh|B>1{l@hlACz^Ygt3lw zosXmBWD?jvm+x{h9iE{5<3UMPyI0xyBd7GQ*LqIcGVxp;&FM2Mc(6IXb_5H;i_8-c zqqid~(fFrK_;FX8my)IOyB6;k96N!f<5f!UnC0uA1&1VC_a9=-e8rCAi$yTc2^Sje zPy58hNJ?VIg@>vqRhrYane{+F_G~2W4#7WYY<)ePUf?lJE)dmwyfk1xTq9!Fw{1=1 z1+TBfiPG1p!}HE&k{r_UpYup=u8|T0v5ZUZY&_;9uP^>M56xK#I-P|9kSHSES|(rt zv8mM;%;ZW(YjTl#7)~Tm8de&vMyS|>!x=##fEHW7{pxwnS zMqWv|N4l*u9n##{C?BfQ;_Y72PJZ599>p%I20*Ov4z%|L{j5C55|XUv4->Ty3)7wV!@;nX(>vEl@iw9PfOFlkFHBE=ZA@C;J<= z<0-GTHy!&+8{d%T!sZJk^lx`Xr9+-CS4FG9+gm5(l+zc{V<**J&(gHJng(?%K2)y} z35;HJ%_(F%u9~S}pWi@m3Q0SO>J>f<`ep5PD@iV2Ta>ntuZ=2dO1+$MaF^M-e$bdK zlqT9w4QCRu$2>eX`yqYKslq7zQa!ikvs&vzgqgd?rayL^GW8FS#AGGs>Far}est*y z4OQ2&VH(zIv=Tp|$>3waBocLvm{wnM<>&TZH4u*qKhmiF>RmKjSZ3isxKG~@OXFY8A!A}f z3Wu*O2(A-#lZP42)hCdYtByu8RTY}4<|@YVIUDEnt!|6iTmXkh=z;upKURzL=eSaE zVp_FCSkegdu)H@m`EBoG?rkR1^~?T#EyNasSrgfBhW;o@>!zFXBf=@goCOmipJE4) z6OFH7Guoj^9bq>L*dUD}OpVnjbGYrG)7C)Ih#-#&9j z^RLL%le~DXC?(!CDaL1dNuKyx)w)1Ai8)`t8dh8PWaM>4jTDbd4V@SK8^c*x5KL2x z8N+Y2#EVfEd?m&c6!HkBn%-Aly(8EwCy-Z_VhWA-hp&gOwx}E=bMIlAC7L-s&DWOm z_W{c>R`XVjw;Q`~0EwKV+ixgwBvYSmx&4(deY*=eX2<2_K^b?R5=4AfGKMp}AmjTI zkX@DhG^$Xk_pY*|B?^zf((Rljf0<{iyDWnqFiH<}$h?c|#goSKmx>pauh>6eeWcw(BT9h)`*#1i`9!o;%$#k+bHKzM-gisv&wz-#a=)`}%>hF^givtFY_5MSj`wHjtH`!u8cu)$O zFfS4ZHs1|-T8?k-0NU6JgaOivrnygsIaa_lR5{$N<64tE>gR{q1K*5f5mAo>^niIz z*5l|-PR-e%-(0Se?xG7cf4m?#ILA+(mm!=>WjXi;5`d|&g&?;N+zaEXc`uKh81OU> z4pl9s+b>Cesqox#Hld=I7^lx7fi~}uGhL4rc77#cF~E@8KIze%`9^LGnEiCyPRoLF zF7#E_l9Sgr&fN1p4A~Eov_=Zh@0=x*vZ$0>Kyteq1 z#SrY!Kjf(pmV{^7yvpV5VxJ|+ypyZ8S?`sd7S@VuntNuV7hga;>QVdwB6*f_z3bO^&lSWUib=06Ej0F~Qz$Y#FO(En^#2 zyXawELiA$UMPt{uv^vE4-GtP7+M~y3BWU5k6e;yS&BnHdP(1(wtTey@W>x^a7SIL$ zy%j2}C~;3=O~tMV^_gR37{y?9>Ad~G+P!8_|KX*S2f`BP#)3>DU3eA1*YdQLL|0_X zQjkbNyw-FITM7T*$TfJ)26^&HnqvL^+GSk*xIod@5e)EqX%6zMbyYX*pFB^CLE|i+ zUR_uRYHfZJFsR{Jh^Gj!UoK}B-gl*Ro;%XkJ5V>^>-yOPf031}svs|1$mJj}yr6Z@ zze-@$(}zkd@0Gnn0)>29j>5waq%_2b9rr;gfO(cU@$UT|+H06&4o z`wr=s5d#`*!(R5N+shv)sZFHesy4GSj;EB10}`TvoIS&R#d@Z{>0oQh@Ocb73rwd( zU-UTTTG5)qrH{iwRAScO@s-xi(sB5uI_{2LYEKCc+OGfvNv+XLIRO-~B1hQftV|{v z?POP&GFz(#{2IYXvsN78!gr!%yuFzXVHFz@2e~hkFgd*u&RSr0fUn7X_xABwon4EW z%!o@?g^;kg7vXQ(V4sIJnOfGaT7q}#QmRdDyO9+Cw@T^$OG|2=?O)b_1V*Y2vN0gz z&9cc-^gNqbi(*G~@-{F0xxv5*IUW9jR`s}DOsT80%%=@i9-d2Py;2vLM|MTI74lx@ zJr=pa=?zxByYYBnjj8iH5?Gmn=>3^=p7m|fNskT9mC$JW0gi`&rrGCnV;=-KN0`#< zzZRSM`WTkUxxFa((kV$6I4ccII6xeNfL~V@gX|}#WDA}iNj0+8cei&s4~ILkgYXHK z3X*bSq z5-ITNE22NCmmRVe#6&Ueh|pBYdPP;~yo~{S-sKFd%jGk7dSYbJRn}tkXta@#j+p4t zJ8qQ>U85a0Zvp=bVBCJ-#qbU{*C{0Q`WpGT0JoalfFaUF-N4oF%$BDi z3CB!`4{%P7Tw}D-40KBP{vjy=Eij(_U)yV#K`;QBhh6CH36;^T2;p)NlxkwXSq$(K z0;-JkZG})Ef7uM`uOL*Off5tnzPc485VvAPkari*+KPQ#w_-Xdy5NCoG|l2$v5?jw z1i#IF`}y}*%}Dl&O>{2fgiht+D^ERYHYNc7iJ43#fc!ms|6o?~{_*XgKGYz6e zY8-+z7C9l+9y63ap?=nE8Xdi)5nk6&O+R-S&K zkaap|wT)-l^JZwY@3~IjY`&I0qFpBleWg9i_R%-H{7qr;oh~9_BdAbxead#JuCR|> z(^F6(o!C!NJ=o>jjk8l#5nqg9_58xN;B*;X1!CJZa0$fx?&mfiP6YKI#*f>Ut52R| zHYb*&0Pxt{x0WAWbSoX}I2BQ&gFx`VpaFTomy+XgWH&21icKA9YLTLb@81l65}W<< zqYP=mQ{Ap>7^C4@dMQ!vXvc^nJ$>HDRvre5@}=kSA>#3!{YTP0Hg`?5f9BW-5whw> zU)Ic8p}4Bt4k~|oMupYtUFhIEsqKXkH}At$X&i{h^-BAOLw11V2Z(ArR{HW5oa+3k z0AsCj{^Nx`r5Xn?IDfViLa{BQ#y6Qyf=ofk9v9tuotJHborUVF;p1fMGLXyY#(E}> zoC<0x2|i@%*|Kd5RrpjA|97=?i;xb>EBFNs1V9A_O`Z<*{SoKDK(oXk^ zqwEBQA5zt9tiE?-4KMpd)LpscESaG^I&HDxJXVj;pI&saJT+MD{Dvg=VZj?g~f*w6cHauMyOqF6p)zf3$D6^5Ya2stZ{hwqf8*Q=Jf>ZhNQ}e_w5_+xR-F1GVMGsODI$BU9Knr%NAMB z+pfvw8w24sbq`4vA=U1zDW#NNz3d*{EZ*}TQ8hYbR(L)7*(7xpB?vX*(4_a|CH z%!N$9ma>gyWa8VW^=6v9V%Ymy;uCYsJN1hkOJtxwqt89Pj=2e{$>=-l;VVR1DC;04 zmC8yahSyBC(hBt_ftGmVAg5!eHhGRbQ1-Q@Zs~no4WQbNG@d$X0IU>|hCQKYYu^B* z(H4cSA-md&6&MOEZS%B?O0-OH?hj*KKXY1i&6l#CpD5n}Qk)u~y=Sr!YsOM0ZE`tz z1s4%jMj((1q_R$Z8sNqe;mWHL8dahfxEKumOj)M|EGI*$9l$8Cnps)!D~C>;Xk2a9re)Vq;{@q3v!HCk%=uLW*k;uBXx1Z6 zN7Qaf?(TG{q5T`2Wy@D9le(d75BXx^DdKp#Nvyi|2$JO^ZwK8Y$xPE$V@SUt35Ar; zz%tLHrvMwJ=i!TJ&FA)k^a=c)0aBWW#78)Qc4TYkOGE3s!o{NTK^kv4-lUeXAO^)G=13IgIl=vy-pv}(S>tlOjorB=^b z^>Ky}KOrlJmdB^n3?Vy_26B96eIqx5k8FBTOxkJTf#Y3=mOe2M((DiG-H|rb>&z)L zuE+9(6rpbKIkHr&t=FcV&1W_pvL95$LHchjTwS@j|C0vA5HfI(+>O-hg!k+Pgx$FF zbNPSS+wz-12X6J}K9~Q;zT^M@r!Kw!*tIBNF`^5RB5U{PtVtuyR%K?G^4GR de.adorsys.opba fintech-examples - 0.30.0.1 + 1.0.0 fintech-api diff --git a/fintech-examples/fintech-api/src/main/resources/static/fintech_api.yml b/fintech-examples/fintech-api/src/main/resources/static/fintech_api.yml index ed24876a14..da37fff661 100644 --- a/fintech-examples/fintech-api/src/main/resources/static/fintech_api.yml +++ b/fintech-examples/fintech-api/src/main/resources/static/fintech_api.yml @@ -211,14 +211,14 @@ paths: get: tags: - FinTechBankSearch - summary: Request the profile of the bank identified with id (bankId). - description: Request the profile of the bank identified with id (bankId). + summary: Request the profile of the bank identified with id (bankProfileId). + description: Request the profile of the bank identified with id (bankProfileId). operationId: bankProfileGET parameters: #header - $ref: "#/components/parameters/X-Request-ID" - $ref: "#/components/parameters/X-XSRF-TOKEN" - - name: bankId + - name: bankProfileId in: query description: Identifier of the bank to be loaded. required: true @@ -256,9 +256,11 @@ paths: #header - $ref: "#/components/parameters/X-Request-ID" - $ref: "#/components/parameters/X-XSRF-TOKEN" + - $ref: "#/components/parameters/X-Psu-Authentication-Required" - $ref: "#/components/parameters/Fintech-Redirect-URL-OK" - $ref: "#/components/parameters/Fintech-Redirect-URL-NOK" - $ref: "#/components/parameters/LoARetrievalInformation" + - $ref: "#/components/parameters/X-Create-Consent-If-None" #query - $ref: "#/components/parameters/withBalance" - $ref: "#/components/parameters/online" @@ -286,6 +288,14 @@ paths: #path - $ref: "#/components/parameters/bank-id" - $ref: "#/components/parameters/account-id" + #header + - $ref: "#/components/parameters/X-Request-ID" + - $ref: "#/components/parameters/X-XSRF-TOKEN" + - $ref: "#/components/parameters/X-Psu-Authentication-Required" + - $ref: "#/components/parameters/Fintech-Redirect-URL-OK" + - $ref: "#/components/parameters/Fintech-Redirect-URL-NOK" + - $ref: "#/components/parameters/LoTRetrievalInformation" + - $ref: "#/components/parameters/X-Create-Consent-If-None" #query - $ref: "#/components/parameters/dateFrom" - $ref: "#/components/parameters/dateTo" @@ -294,13 +304,6 @@ paths: - $ref: "#/components/parameters/deltaList" - $ref: "#/components/parameters/online" - #header - - $ref: "#/components/parameters/X-Request-ID" - - $ref: "#/components/parameters/X-XSRF-TOKEN" - - $ref: "#/components/parameters/Fintech-Redirect-URL-OK" - - $ref: "#/components/parameters/Fintech-Redirect-URL-NOK" - - $ref: "#/components/parameters/LoTRetrievalInformation" - security: - sessionCookie: [] responses: @@ -401,7 +404,7 @@ paths: #header - $ref: "#/components/parameters/X-Request-ID" - $ref: "#/components/parameters/X-XSRF-TOKEN" - - $ref: "#/components/parameters/X-Pis-Psu-Authentication-Required" + - $ref: "#/components/parameters/X-Psu-Authentication-Required" - $ref: "#/components/parameters/Fintech-Redirect-URL-OK" - $ref: "#/components/parameters/Fintech-Redirect-URL-NOK" @@ -418,6 +421,31 @@ paths: "401": $ref: "#/components/responses/401_Unauthorized" + /v1/ais/banks/{bank-id}/consents: + delete: + tags: + - FinTechAccountInformation + operationId: aisConsentsDELETE + summary: Deletes all consents that are associated with bank + description: Deletes all consents that are associated with bank + parameters: + #path + - $ref: "#/components/parameters/bank-id" + + #header + - $ref: "#/components/parameters/X-Request-ID" + - $ref: "#/components/parameters/X-XSRF-TOKEN" + + security: + - sessionCookie: [ ] + responses: + "200": + $ref: "#/components/responses/200_ConsentRemovalResult" + "401": + $ref: "#/components/responses/401_Unauthorized" + "404": + $ref: "#/components/responses/404_NotFound" + /v1/consent/{userid}/{password}: get: summary: ask for existing consent of user @@ -579,6 +607,15 @@ components: - "FROM_TPP_WITH_AVAILABLE_CONSENT" - "FROM_TPP_WITH_NEW_CONSENT" + X-Create-Consent-If-None: + name: X-Create-Consent-If-None + required: false + in: header + schema: + type: string + description: json representation of AisConsentRequest object + example: {"access":{"allPsd2":"ALL_ACCOUNTS_WITH_BALANCES"},"frequencyPerDay":4,"validUntil":"2021-06-18","recurringIndicator":true,"combinedServiceIndicator":false} + X-Request-ID: name: X-Request-ID in: header @@ -591,8 +628,19 @@ components: type: string format: uuid - X-Pis-Psu-Authentication-Required: - name: X-Pis-Psu-Authentication-Required + X-Ais-Psu-Authentication-Required: + name: X-Ais-Psu-Authentication-Required + in: header + description: | + If false, login form to OPBA will not be displayed, so + that authentication is not necessary. If absent or true - login form for consents will be displayed. + example: "true" + schema: + type: boolean + default: "true" + + X-Psu-Authentication-Required: + name: X-Psu-Authentication-Required in: header description: | If false, login form to OPBA will not be displayed as there might be nothing to share for payments, so @@ -958,6 +1006,13 @@ components: schema: type: object + 200_ConsentRemovalResult: + description: List of all removed consents + content: + application/json: + schema: + type: object + schemas: paymentInitiationWithStatusResponse: description: response from open banking gateway @@ -1089,6 +1144,11 @@ components: type: string uuid: type: string + format: uuid + profiles: + type: array + items: + $ref: "#/components/schemas/bankProfile" bankProfile: title: Bank Profile @@ -1098,13 +1158,26 @@ components: type: string bankName: type: string + name: + type: string bic: type: string + uuid: + type: string + format: uuid services: type: array items: # can be an enum type: string + externalId: + type: string + externalInterfaces: + type: string + protocolType: + type: string + isSandbox: + type: boolean # Account and transaction information accountDetails: @@ -1234,6 +1307,67 @@ components: $ref: "#/components/schemas/transactionList" pending: $ref: "#/components/schemas/transactionList" + rawTransactions: + description: Raw bank response as String + type: string + + analyticsReportDetails: + description: | + JSON based analytics report. + This account report contains transaction categorization result. + type: object + properties: + transactionId: + description: The id of transaction this analytics result refers to. + type: string + mainCategory: + description: Main category of the booking. + type: string + subCategory: + description: Sub category of the booking. + type: string + specification: + description: Specification of the booking. + type: string + otherAccount: + description: Related account. + type: string + logo: + description: Logo. + type: string + homepage: + description: Homepage. + type: string + hotline: + description: Hotline. + type: string + email: + description: Email. + type: string + custom: + description: Custom information about analyzed transaction. + type: object + additionalProperties: + type: string + usedRules: + description: Rules that were used to analyze. + type: array + uniqueItems: true + items: + type: string + nextBookingDate: + description: Classification next booking date. + type: string + format: date + cycle: + description: Classification cycle result. + type: string + + analyticsReport: + description: Array of transaction details. + type: array + items: + $ref: "#/components/schemas/analyticsReportDetails" accountStatus: description: | @@ -1871,6 +2005,8 @@ components: $ref: "#/components/schemas/accountReference" transactions: $ref: "#/components/schemas/accountReport" + analytics: + $ref: "#/components/schemas/analyticsReport" ultimateCreditor: description: Ultimate Creditor. diff --git a/fintech-examples/fintech-db-schema/pom.xml b/fintech-examples/fintech-db-schema/pom.xml index 8e5b748931..94d4de6904 100644 --- a/fintech-examples/fintech-db-schema/pom.xml +++ b/fintech-examples/fintech-db-schema/pom.xml @@ -6,7 +6,7 @@ de.adorsys.opba fintech-examples - 0.30.0.1 + 1.0.0 jar diff --git a/fintech-examples/fintech-example-tests/pom.xml b/fintech-examples/fintech-example-tests/pom.xml index 46b4f03799..f9c5563c52 100644 --- a/fintech-examples/fintech-example-tests/pom.xml +++ b/fintech-examples/fintech-example-tests/pom.xml @@ -5,7 +5,7 @@ fintech-examples de.adorsys.opba - 0.30.0.1 + 1.0.0 4.0.0 diff --git a/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/FintechConsentUiSmokeE2ETest.java b/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/FintechConsentUiSmokeE2ETest.java index 61bc144244..71a59fa5ea 100644 --- a/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/FintechConsentUiSmokeE2ETest.java +++ b/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/FintechConsentUiSmokeE2ETest.java @@ -68,7 +68,7 @@ static void setupDriverArch() { void testRedirectUserWantsToSeeItsAccountsFromFintech(FirefoxDriver firefoxDriver) { given().create_new_user_in_sandbox_tpp_management(username, PIN) .enabled_redirect_sandbox_mode(smokeConfig.getAspspProfileServerUri()) - .fintech_points_to_fintechui_login_page(smokeConfig.getFintechServerUri()); + .fintech_points_to_fintech_server(smokeConfig.getFintechServerUri()); when().user_already_login_in_bank_profile(firefoxDriver, username, fintech_login, REDIRECT_MODE, username) .and() @@ -110,7 +110,7 @@ void testRedirectUserWantsToSeeItsAccountsFromFintech(FirefoxDriver firefoxDrive public void testEmbeddedUserWantsItsAccountsFromFintech(FirefoxDriver firefoxDriver) { given().create_new_user_in_sandbox_tpp_management(username, PIN) .enabled_embedded_sandbox_mode(smokeConfig.getAspspProfileServerUri()) - .fintech_points_to_fintechui_login_page(smokeConfig.getFintechServerUri()); + .fintech_points_to_fintech_server(smokeConfig.getFintechServerUri()); when().user_consent_authorization_in_embedded_mode(firefoxDriver, username, fintech_login, EMBEDDED_MODE, username); @@ -122,7 +122,7 @@ public void testEmbeddedUserWantsItsAccountsFromFintech(FirefoxDriver firefoxDri void testEmbeddedUserWantsToSeeItsAccountsFromFintech(FirefoxDriver firefoxDriver) { given().create_new_user_in_sandbox_tpp_management(username, PIN) .enabled_embedded_sandbox_mode(smokeConfig.getAspspProfileServerUri()) - .fintech_points_to_fintechui_login_page(smokeConfig.getFintechServerUri()); + .fintech_points_to_fintech_server(smokeConfig.getFintechServerUri()); when().user_already_login_in_bank_profile(firefoxDriver, username, fintech_login, EMBEDDED_MODE, username) .and() .user_for_embeeded_provided_to_consent_ui_initial_parameters_to_list_transactions_with_all_accounts_consent(firefoxDriver, username) @@ -150,7 +150,7 @@ void testEmbeddedUserWantsToSeeItsAccountsFromFintech(FirefoxDriver firefoxDrive public void testUserAfterLoginWantsToLogout(FirefoxDriver firefoxDriver) { given().create_new_user_in_sandbox_tpp_management(username, PIN) .enabled_embedded_sandbox_mode(smokeConfig.getAspspProfileServerUri()) - .fintech_points_to_fintechui_login_page(smokeConfig.getFintechServerUri()); + .fintech_points_to_fintech_server(smokeConfig.getFintechServerUri()); when().user_opens_fintechui_login_page(firefoxDriver) .and() @@ -160,7 +160,7 @@ public void testUserAfterLoginWantsToLogout(FirefoxDriver firefoxDriver) { .and() .user_looks_for_a_bank_in_the_bank_search_input_place(firefoxDriver, ADORSYS_XS2A) .and() - .user_wait_for_the_result_in_bank_search(firefoxDriver) + .user_wait_for_the_result_in_bank_search_and_select(firefoxDriver, REDIRECT_MODE) .and() .user_navigates_to_page(firefoxDriver) .and() @@ -178,9 +178,9 @@ public void testUserAfterLoginWantsToLogout(FirefoxDriver firefoxDriver) { public void testRedirectUserToSeeItsAccountsFromFintech(FirefoxDriver firefoxDriver) { given().create_new_user_in_sandbox_tpp_management(username, PIN) .enabled_embedded_sandbox_mode(smokeConfig.getAspspProfileServerUri()) - .fintech_points_to_fintechui_login_page(smokeConfig.getFintechServerUri()); + .fintech_points_to_fintech_server(smokeConfig.getFintechServerUri()); - when().user_authorizes_payment_in_redirect_mode(firefoxDriver, username, fintech_login, REDIRECT_MODE, username); + when().user_authorizes_payment_in_redirect_mode(firefoxDriver, username, fintech_login, ADORSYS_XS2A, username); then().fintech_can_read_user_accounts(); } diff --git a/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/FintechPaymentSmokeE2ETest.java b/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/FintechPaymentSmokeE2ETest.java index a192960e92..bfb333411c 100644 --- a/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/FintechPaymentSmokeE2ETest.java +++ b/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/FintechPaymentSmokeE2ETest.java @@ -75,9 +75,9 @@ static void setupDriverArch() { void testUserAfterInitiatingAPaymentWantsToSeeItsTransactionsOnFintechRedirectMode(FirefoxDriver firefoxDriver) { given().create_new_user_in_sandbox_tpp_management(username, PIN) .enabled_redirect_sandbox_mode(smokeConfig.getAspspProfileServerUri()) - .fintech_points_to_fintechui_login_page(smokeConfig.getFintechServerUri()); + .fintech_points_to_fintech_server(smokeConfig.getFintechServerUri()); - when().user_authorizes_payment_in_redirect_mode(firefoxDriver, username, fintech_login, REDIRECT_MODE, username) + when().user_authorizes_payment_in_redirect_mode(firefoxDriver, username, fintech_login, ADORSYS_XS2A, username) .and() .user_navigates_to_page(firefoxDriver) .and() @@ -121,7 +121,7 @@ void testUserAfterInitiatingAPaymentWantsToSeeItsTransactionsOnFintechRedirectMo void testUserAfterInitiatingAPaymentWantsToSeeItsTransactionsOnFintechEmbeddedMode(FirefoxDriver firefoxDriver) { given().create_new_user_in_sandbox_tpp_management(username, PIN) .enabled_redirect_sandbox_mode(smokeConfig.getAspspProfileServerUri()) - .fintech_points_to_fintechui_login_page(smokeConfig.getFintechServerUri()); + .fintech_points_to_fintech_server(smokeConfig.getFintechServerUri()); when().user_consent_authorization_in_embedded_mode(firefoxDriver, username, fintech_login, EMBEDDED_MODE, username) .and() diff --git a/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/FintechServer.java b/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/FintechServer.java index 6b138bf5e8..e5a12ded85 100644 --- a/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/FintechServer.java +++ b/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/FintechServer.java @@ -62,8 +62,8 @@ void prepareRestAssured() { RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); RestAssured.config = RestAssured.config().redirect(RedirectConfig.redirectConfig().followRedirects(false)); } - public SELF fintech_points_to_fintechui_login_page(String fintechUri) { - RestAssured.baseURI = fintechUri; + public SELF fintech_points_to_fintech_server(String fintechServerUri) { + RestAssured.baseURI = fintechServerUri; RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); return self(); } diff --git a/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/FintechStagesUtils.java b/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/FintechStagesUtils.java index 30f0f8fc39..9202b72c7f 100644 --- a/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/FintechStagesUtils.java +++ b/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/FintechStagesUtils.java @@ -15,9 +15,8 @@ public class FintechStagesUtils { public static final String TRANSACTION = "/accounts"; public static final String BANKSEARCH_LOGIN = "/login"; public static final String ADORSYS_XS2A = "adorsys xs2a"; - public static final String FINTECH_UI_URI = "https://obg-dev-fintechui.cloud.adorsys.de"; - public static final String REDIRECT_MODE = "adorsys redirect"; - public static final String EMBEDDED_MODE = "adorsys embedded"; + public static final String REDIRECT_MODE = "redirect"; + public static final String EMBEDDED_MODE = "embedded"; public static RequestSpecification withDefaultHeaders() { diff --git a/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/WebDriverBasedUserInfoFintech.java b/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/WebDriverBasedUserInfoFintech.java index 350a6b9535..407866234a 100644 --- a/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/WebDriverBasedUserInfoFintech.java +++ b/fintech-examples/fintech-example-tests/src/test/java/de/adorsys/fintech/tests/e2e/steps/WebDriverBasedUserInfoFintech.java @@ -2,7 +2,11 @@ import com.tngtech.jgiven.integration.spring.JGivenStage; import de.adorsys.opba.protocol.xs2a.tests.e2e.sandbox.servers.WebDriverBasedAccountInformation; -import org.openqa.selenium.*; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import org.springframework.beans.factory.annotation.Autowired; @@ -13,7 +17,7 @@ import java.net.URI; import java.time.Duration; -import static de.adorsys.fintech.tests.e2e.steps.FintechStagesUtils.FINTECH_UI_URI; +import static de.adorsys.fintech.tests.e2e.steps.FintechStagesUtils.ADORSYS_XS2A; import static de.adorsys.fintech.tests.e2e.steps.FintechStagesUtils.PIN; import static de.adorsys.opba.protocol.xs2a.tests.e2e.sandbox.servers.config.RetryableConfig.TEST_RETRY_OPS; @@ -30,8 +34,11 @@ public class WebDriverBasedUserInfoFintech fintech-examples de.adorsys.opba - 0.30.0.1 + 1.0.0 4.0.0 diff --git a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/controller/FinTechAccountInformationImpl.java b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/controller/FinTechAccountInformationImpl.java index 4fc132fbb5..2220c46bb9 100644 --- a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/controller/FinTechAccountInformationImpl.java +++ b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/controller/FinTechAccountInformationImpl.java @@ -7,6 +7,7 @@ import de.adorsys.opba.fintech.impl.controller.utils.LoTRetrievalInformation; import de.adorsys.opba.fintech.impl.database.entities.SessionEntity; import de.adorsys.opba.fintech.impl.service.AccountService; +import de.adorsys.opba.fintech.impl.service.ConsentService; import de.adorsys.opba.fintech.impl.service.SessionLogicService; import de.adorsys.opba.fintech.impl.service.TransactionService; import lombok.RequiredArgsConstructor; @@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; +import java.util.Map; import java.util.UUID; @Slf4j @@ -26,34 +28,48 @@ public class FinTechAccountInformationImpl implements FinTechAccountInformationA private final SessionLogicService sessionLogicService; private final AccountService accountService; private final TransactionService transactionService; + private final ConsentService consentService; @Override public ResponseEntity aisAccountsGET(String bankId, UUID xRequestID, String xsrfToken, String fintechRedirectURLOK, String fintechRedirectURLNOK, - String loARetrievalInformation, Boolean withBalance, Boolean online) { + String loARetrievalInformation, Boolean xPsuAuthenticationRequired, + String createConsentIfNone, Boolean withBalance, Boolean online) { if (!sessionLogicService.isSessionAuthorized()) { log.warn("aisAccountsGET failed: user is not authorized!"); return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); } SessionEntity sessionEntity = sessionLogicService.getSession(); - return sessionLogicService.addSessionMaxAgeToHeader(accountService.listAccounts(sessionEntity, - fintechRedirectURLOK, fintechRedirectURLNOK, bankId, LoARetrievalInformation.valueOf(loARetrievalInformation), withBalance, online)); + return sessionLogicService.addSessionMaxAgeToHeader( + accountService.listAccounts(sessionEntity, fintechRedirectURLOK, fintechRedirectURLNOK, + bankId, LoARetrievalInformation.valueOf(loARetrievalInformation), + createConsentIfNone, withBalance, xPsuAuthenticationRequired, online)); } @Override public ResponseEntity aisTransactionsGET(String bankId, String accountId, UUID xRequestID, String xsrfToken, String fintechRedirectURLOK, String fintechRedirectURLNOK, String loTRetrievalInformation, - LocalDate dateFrom, LocalDate dateTo, - String entryReferenceFrom, String bookingStatus, Boolean deltaList, Boolean online) { + Boolean xPsuAuthenticationRequired, String createConsentIfNone, + LocalDate dateFrom, LocalDate dateTo, String entryReferenceFrom, String bookingStatus, + Boolean deltaList, Boolean online) { if (!sessionLogicService.isSessionAuthorized()) { log.warn("aisTransactionsGET failed: user is not authorized!"); return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); } SessionEntity sessionEntity = sessionLogicService.getSession(); return sessionLogicService.addSessionMaxAgeToHeader( - transactionService.listTransactions(sessionEntity, fintechRedirectURLOK, fintechRedirectURLNOK, - bankId, accountId, dateFrom, dateTo, entryReferenceFrom, bookingStatus, deltaList, LoTRetrievalInformation.valueOf(loTRetrievalInformation), online)); + transactionService.listTransactions(sessionEntity, fintechRedirectURLOK, fintechRedirectURLNOK, + bankId, accountId, createConsentIfNone, dateFrom, dateTo, + entryReferenceFrom, bookingStatus, deltaList, + LoTRetrievalInformation.valueOf(loTRetrievalInformation), xPsuAuthenticationRequired, online)); + } + + @Override + public ResponseEntity aisConsentsDELETE(String bankId, UUID xRequestID, String xsrfToken) { + SessionEntity sessionEntity = sessionLogicService.getSession(); + consentService.deleteAllConsentsOfBank(sessionEntity, bankId); + return ResponseEntity.ok().body(Map.of()); } } diff --git a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/controller/FinTechBankSearchImpl.java b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/controller/FinTechBankSearchImpl.java index 4fdb3245da..25349ea0f2 100644 --- a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/controller/FinTechBankSearchImpl.java +++ b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/controller/FinTechBankSearchImpl.java @@ -32,11 +32,11 @@ public ResponseEntity bankSearchGET(UUID xRequestID, String } @Override - public ResponseEntity bankProfileGET(UUID xRequestID, String fintechToken, String bankId) { + public ResponseEntity bankProfileGET(UUID xRequestID, String fintechToken, String bankProfileId) { if (!sessionLogicService.isSessionAuthorized()) { log.warn("bankProfileGET failed: user is not authorized!"); return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); } - return sessionLogicService.addSessionMaxAgeToHeader(new ResponseEntity<>(bankSearchService.searchBankProfile(bankId), HttpStatus.OK)); + return sessionLogicService.addSessionMaxAgeToHeader(new ResponseEntity<>(bankSearchService.searchBankProfile(bankProfileId), HttpStatus.OK)); } } diff --git a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/database/repositories/ConsentRepository.java b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/database/repositories/ConsentRepository.java index 64f6615c3d..5b5fc7e392 100644 --- a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/database/repositories/ConsentRepository.java +++ b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/database/repositories/ConsentRepository.java @@ -3,6 +3,7 @@ import de.adorsys.opba.fintech.impl.database.entities.ConsentEntity; import de.adorsys.opba.fintech.impl.database.entities.UserEntity; import de.adorsys.opba.fintech.impl.tppclients.ConsentType; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.repository.CrudRepository; import java.util.List; @@ -18,4 +19,7 @@ List findListByUserEntityAndBankIdAndConsentTypeAndConsentConfirm bankId, ConsentType consentType, Boolean consentConfirmed); List findByUserEntityAndConsentTypeAndConsentConfirmedOrderByCreationTimeDesc(UserEntity userEntity, ConsentType consentType, Boolean consentConfirmed); + + @Modifying + long deleteByUserEntityAndBankId(UserEntity entity, String bankId); } diff --git a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/AccountService.java b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/AccountService.java index 19c7454bb3..7989073213 100644 --- a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/AccountService.java +++ b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/AccountService.java @@ -13,26 +13,29 @@ import de.adorsys.opba.fintech.impl.tppclients.AisErrorDecoder; import de.adorsys.opba.fintech.impl.tppclients.ConsentType; import de.adorsys.opba.fintech.impl.tppclients.TppAisClient; -import de.adorsys.opba.tpp.banksearch.api.model.generated.BankProfileResponse; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import java.util.Map; import java.util.Optional; import java.util.UUID; import static de.adorsys.opba.fintech.impl.tppclients.Consts.COMPUTE_FINTECH_ID; import static de.adorsys.opba.fintech.impl.tppclients.Consts.COMPUTE_X_REQUEST_SIGNATURE; import static de.adorsys.opba.fintech.impl.tppclients.Consts.COMPUTE_X_TIMESTAMP_UTC; +import static de.adorsys.opba.fintech.impl.tppclients.Consts.HEADER_COMPUTE_PSU_IP_ADDRESS; @Service @Slf4j @RequiredArgsConstructor public class AccountService { + private final FintechUiConfig uiConfig; private final TppAisClient tppAisClient; private final RestRequestContext restRequestContext; @@ -44,13 +47,14 @@ public class AccountService { public ResponseEntity listAccounts(SessionEntity sessionEntity, String fintechOkUrl, String fintechNOKUrl, - String bankId, LoARetrievalInformation loARetrievalInformation, boolean withBalance, Boolean online) { + String bankId, LoARetrievalInformation loARetrievalInformation, String createConsentIfNone, + boolean withBalance, Boolean psuAuthenticationRequired, Boolean online) { log.info("List of accounts {} with balance {}", loARetrievalInformation, withBalance); final String fintechRedirectCode = UUID.randomUUID().toString(); try { - ResponseEntity accounts = readOpbaResponse(bankId, sessionEntity, fintechRedirectCode, loARetrievalInformation, withBalance, online); + ResponseEntity accounts = readOpbaResponse(bankId, sessionEntity, fintechRedirectCode, loARetrievalInformation, createConsentIfNone, withBalance, psuAuthenticationRequired, online); switch (accounts.getStatusCode()) { case OK: @@ -67,13 +71,14 @@ public ResponseEntity listAccounts(SessionEntity sessionEntity, } catch (ConsentException consentException) { HttpHeaders headers = new HttpHeaders(); headers.add(AisErrorDecoder.X_ERROR_CODE, consentException.getXErrorCode()); - return new ResponseEntity(headers, HttpStatus.valueOf(consentException.getHttpResponseCode())); + return new ResponseEntity<>(headers, HttpStatus.valueOf(consentException.getHttpResponseCode())); } } - + @SneakyThrows private ResponseEntity readOpbaResponse(String bankID, SessionEntity sessionEntity, String redirectCode, - LoARetrievalInformation loARetrievalInformation, boolean withBalance, Boolean online) { + LoARetrievalInformation loARetrievalInformation, String createConsentIfNone, + boolean withBalance, Boolean psuAuthenticationRequired, Boolean online) { UUID xRequestId = UUID.fromString(restRequestContext.getRequestId()); Optional optionalConsent = Optional.empty(); if (loARetrievalInformation.equals(LoARetrievalInformation.FROM_TPP_WITH_AVAILABLE_CONSENT)) { @@ -86,22 +91,22 @@ private ResponseEntity readOpbaResponse(String bankID, SessionEntity sessionEnti optionalConsent.get().getUserEntity().getLoginUserName(), optionalConsent.get().getBankId(), optionalConsent.get().getCreationTime()); - return consentAvailable(bankID, sessionEntity, redirectCode, xRequestId, optionalConsent, withBalance, online); + return consentAvailable(bankID, sessionEntity, redirectCode, xRequestId, optionalConsent, createConsentIfNone, withBalance, psuAuthenticationRequired, online); } - BankProfileResponse bankProfile = searchService.getBankProfileById(bankID).getBody(); - if (null != bankProfile.getBankProfileDescriptor().getConsentSupportByService() - && "true".equals(bankProfile.getBankProfileDescriptor().getConsentSupportByService().get(Actions.LIST_ACCOUNTS.name()))) { + Map consentSupportByService = searchService.getBankProfileById(bankID).getBody().getBankProfileDescriptor().getConsentSupportByService(); + if (null != consentSupportByService && "true".equals(consentSupportByService.get(Actions.LIST_ACCOUNTS.name()))) { log.info("LoA no valid ais consent for user {} bank {} available", sessionEntity.getUserEntity().getLoginUserName(), bankID); - return consentNotYetAvailable(bankID, sessionEntity, redirectCode, xRequestId, optionalConsent); + return consentNotYetAvailable(bankID, sessionEntity, redirectCode, xRequestId, psuAuthenticationRequired, optionalConsent, withBalance, online, createConsentIfNone); } - return consentAvailable(bankID, sessionEntity, redirectCode, xRequestId, optionalConsent, withBalance, online); + return consentAvailable(bankID, sessionEntity, redirectCode, xRequestId, optionalConsent, createConsentIfNone, withBalance, psuAuthenticationRequired, online); } - private ResponseEntity consentAvailable(String bankID, SessionEntity sessionEntity, String redirectCode, - UUID xRequestId, Optional optionalConsent, boolean withBalance, Boolean online) { - log.info("do LOA for bank {} {} consent", bankID, optionalConsent.isPresent() ? "with" : "without"); + private ResponseEntity consentAvailable(String bankProfileID, SessionEntity sessionEntity, String redirectCode, + UUID xRequestId, Optional optionalConsent, String createConsentIfNone, boolean withBalance, + Boolean psuAuthenticationRequired, Boolean online) { + log.info("do LOA for bank {} {} consent", bankProfileID, optionalConsent.isPresent() ? "with" : "without"); UUID serviceSessionID = optionalConsent.map(ConsentEntity::getTppServiceSessionId).orElse(null); return tppAisClient.getAccounts( tppProperties.getServiceSessionPassword(), @@ -112,16 +117,23 @@ private ResponseEntity consentAvailable(String bankID, SessionEntity sessionEnti COMPUTE_X_TIMESTAMP_UTC, COMPUTE_X_REQUEST_SIGNATURE, COMPUTE_FINTECH_ID, - bankID, + UUID.fromString(bankProfileID), + psuAuthenticationRequired, serviceSessionID, + createConsentIfNone, + null, + HEADER_COMPUTE_PSU_IP_ADDRESS, + null, withBalance, online); } - private ResponseEntity consentNotYetAvailable(String bankID, SessionEntity sessionEntity, String redirectCode, UUID xRequestId, Optional optionalConsent) { - log.info("do LOT (instead of loa) for bank {} {} consent", bankID, optionalConsent.isPresent() ? "with" : "without"); + private ResponseEntity consentNotYetAvailable(String bankProfileID, SessionEntity sessionEntity, String redirectCode, UUID xRequestId, + Boolean psuAuthenticationRequired, Optional optionalConsent, + boolean withBalance, Boolean online, String createConsentIfNone) { + log.info("do LOT (instead of loa) for bank {} {} consent", bankProfileID, optionalConsent.isPresent() ? "with" : "without"); UUID serviceSessionID = optionalConsent.map(ConsentEntity::getTppServiceSessionId).orElse(null); - return tppAisClient.getTransactionsWithoutAccountId( + var response = tppAisClient.getTransactionsWithoutAccountId( tppProperties.getServiceSessionPassword(), sessionEntity.getUserEntity().getFintechUserId(), RedirectUrlsEntity.buildOkUrl(uiConfig, redirectCode), @@ -130,6 +142,32 @@ private ResponseEntity consentNotYetAvailable(String bankID, SessionEntity sessi COMPUTE_X_TIMESTAMP_UTC, COMPUTE_X_REQUEST_SIGNATURE, COMPUTE_FINTECH_ID, - bankID, serviceSessionID, null, null, null, null, null); + UUID.fromString(bankProfileID), + psuAuthenticationRequired, + serviceSessionID, + createConsentIfNone, + null, + HEADER_COMPUTE_PSU_IP_ADDRESS, + null, + null, + null, + null, + null, + null, null, null + ); + if (response.getStatusCode() != HttpStatus.OK) { + return response; + } + return consentAvailable( + bankProfileID, + sessionEntity, + redirectCode, + UUID.randomUUID(), + optionalConsent, + createConsentIfNone, + withBalance, + psuAuthenticationRequired, + online + ); } } diff --git a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/BankSearchService.java b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/BankSearchService.java index 9d684ac9e9..21dfca0a86 100644 --- a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/BankSearchService.java +++ b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/BankSearchService.java @@ -35,12 +35,13 @@ public InlineResponse2001 searchBank(String keyword, Integer start, Integer max) BankSearchResponse bankSearchResponse = tppBankSearchClient.bankSearchGET( xRequestId, - keyword, COMPUTE_X_TIMESTAMP_UTC, COMPUTE_X_REQUEST_SIGNATURE, COMPUTE_FINTECH_ID, + keyword, start, - max).getBody(); + max, + true).getBody(); InlineResponse2001 inlineResponse2001 = new InlineResponse2001().bankDescriptor(Collections.emptyList()); if (bankSearchResponse.getBankDescriptor() != null) { @@ -59,13 +60,14 @@ public InlineResponse2002 searchBankProfile(String bankId) { return new InlineResponse2002().bankProfile(ManualMapper.fromTppToFintech(getBankProfileById(bankId).getBody().getBankProfileDescriptor())); } - public ResponseEntity getBankProfileById(String bankId) { + public ResponseEntity getBankProfileById(String bankProfileId) { UUID xRequestId = UUID.fromString(restRequestContext.getRequestId()); return tppBankSearchClient.bankProfileGET( xRequestId, - bankId, + UUID.fromString(bankProfileId), COMPUTE_X_TIMESTAMP_UTC, COMPUTE_X_REQUEST_SIGNATURE, - COMPUTE_FINTECH_ID); + COMPUTE_FINTECH_ID, + true); } } diff --git a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/ConsentService.java b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/ConsentService.java index 090e690ad6..0f876c2e83 100644 --- a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/ConsentService.java +++ b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/ConsentService.java @@ -1,5 +1,6 @@ package de.adorsys.opba.fintech.impl.service; +import de.adorsys.opba.fintech.impl.database.entities.SessionEntity; import de.adorsys.opba.fintech.impl.database.repositories.ConsentRepository; import de.adorsys.opba.fintech.impl.properties.TppProperties; import de.adorsys.opba.fintech.impl.tppclients.TppConsentClient; @@ -9,6 +10,7 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import javax.transaction.Transactional; import java.util.UUID; import static de.adorsys.opba.fintech.impl.tppclients.Consts.COMPUTE_FINTECH_ID; @@ -50,4 +52,9 @@ public boolean confirmPayment(String authId, UUID xRequestId) { log.debug("consent confirmation response code: {}", statusCode); return statusCode.is2xxSuccessful(); } + + @Transactional + public void deleteAllConsentsOfBank(SessionEntity sessionEntity, String bankId) { + consentRepository.deleteByUserEntityAndBankId(sessionEntity.getUserEntity(), bankId); + } } diff --git a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/HandleAcceptedService.java b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/HandleAcceptedService.java index 47dee08f89..61dbeb1c11 100644 --- a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/HandleAcceptedService.java +++ b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/HandleAcceptedService.java @@ -52,7 +52,7 @@ ResponseEntity handleAccepted(ConsentRepository consentRepository, ConsentType c consentRepository.save(consent); URI location = headers.getLocation(); - log.info("call was accepted, but redirect has to be done for authID:{} location:{}", consent.getTppAuthId(), location); + log.info("call was accepted, but redirect has to be done for serviceSessionId:{} authID:{} location:{}", consent.getTppServiceSessionId(), consent.getTppAuthId(), location); HttpHeaders responseHeaders = sessionLogicService.startRedirect(sessionEntity.getUserEntity(), consent.getTppAuthId()); responseHeaders.add(FIN_TECH_REDIRECT_CODE, fintechRedirectCode); diff --git a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/PaymentService.java b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/PaymentService.java index 424955ae43..1eae42ae94 100644 --- a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/PaymentService.java +++ b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/PaymentService.java @@ -32,6 +32,7 @@ import static de.adorsys.opba.fintech.impl.tppclients.Consts.COMPUTE_FINTECH_ID; import static de.adorsys.opba.fintech.impl.tppclients.Consts.COMPUTE_X_REQUEST_SIGNATURE; import static de.adorsys.opba.fintech.impl.tppclients.Consts.COMPUTE_X_TIMESTAMP_UTC; +import static de.adorsys.opba.fintech.impl.tppclients.Consts.HEADER_COMPUTE_PSU_IP_ADDRESS; @Slf4j @Service @@ -51,7 +52,7 @@ public class PaymentService { private final HandleAcceptedService handleAcceptedService; private final PaymentRepository paymentRepository; - public ResponseEntity initiateSinglePayment(String bankId, String accountId, SinglePaymentInitiationRequest singlePaymentInitiationRequest, + public ResponseEntity initiateSinglePayment(String bankProfileId, String accountId, SinglePaymentInitiationRequest singlePaymentInitiationRequest, String fintechOkUrl, String fintechNOkUrl, Boolean xPisPsuAuthenticationRequired) { log.info("fill paramemeters for payment"); final String fintechRedirectCode = UUID.randomUUID().toString(); @@ -76,21 +77,23 @@ public ResponseEntity initiateSinglePayment(String bankId, String accountI COMPUTE_X_TIMESTAMP_UTC, COMPUTE_X_REQUEST_SIGNATURE, COMPUTE_FINTECH_ID, - bankId, - xPisPsuAuthenticationRequired); + UUID.fromString(bankProfileId), + xPisPsuAuthenticationRequired, + HEADER_COMPUTE_PSU_IP_ADDRESS, + null); if (responseOfTpp.getStatusCode() != HttpStatus.ACCEPTED) { throw new RuntimeException("Did expect status 202 from tpp, but got " + responseOfTpp.getStatusCodeValue()); } redirectHandlerService.registerRedirectStateForSession(fintechRedirectCode, fintechOkUrl, fintechNOkUrl); - return handleAcceptedService.handleAccepted(paymentRepository, ConsentType.PIS, bankId, accountId, fintechRedirectCode, sessionEntity, + return handleAcceptedService.handleAccepted(paymentRepository, ConsentType.PIS, bankProfileId, accountId, fintechRedirectCode, sessionEntity, responseOfTpp.getHeaders()); } - public ResponseEntity> retrieveAllSinglePayments(String bankId, String accountId) { + public ResponseEntity> retrieveAllSinglePayments(String bankProfileID, String accountId) { SessionEntity sessionEntity = sessionLogicService.getSession(); // TODO https://app.zenhub.com/workspaces/open-banking-gateway-5dd3b3daf010250001260675/issues/adorsys/open-banking-gateway/812 // TODO https://app.zenhub.com/workspaces/open-banking-gateway-5dd3b3daf010250001260675/issues/adorsys/open-banking-gateway/794 - List payments = paymentRepository.findByUserEntityAndBankIdAndAccountIdAndPaymentConfirmed(sessionEntity.getUserEntity(), bankId, accountId, true); + List payments = paymentRepository.findByUserEntityAndBankIdAndAccountIdAndPaymentConfirmed(sessionEntity.getUserEntity(), bankProfileID, accountId, true); if (payments.isEmpty()) { return new ResponseEntity<>(new ArrayList<>(), HttpStatus.OK); } @@ -98,13 +101,12 @@ public ResponseEntity> retrieveAllSing for (PaymentEntity payment : payments) { PaymentInformationResponse body = tppPisPaymentStatusClient.getPaymentInformation(tppProperties.getServiceSessionPassword(), - sessionEntity.getUserEntity().getFintechUserId(), UUID.fromString(restRequestContext.getRequestId()), paymentProduct, COMPUTE_X_TIMESTAMP_UTC, COMPUTE_X_REQUEST_SIGNATURE, COMPUTE_FINTECH_ID, - bankId, + UUID.fromString(bankProfileID), payment.getTppServiceSessionId()).getBody(); PaymentInitiationWithStatusResponse paymentInitiationWithStatusResponse = Mappers .getMapper(PaymentInitiationWithStatusResponseMapper.class) diff --git a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/TransactionService.java b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/TransactionService.java index 76277ddba4..9ece6bf5b8 100644 --- a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/TransactionService.java +++ b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/service/TransactionService.java @@ -1,5 +1,6 @@ package de.adorsys.opba.fintech.impl.service; +import com.fasterxml.jackson.databind.ObjectMapper; import de.adorsys.opba.fintech.impl.config.FintechUiConfig; import de.adorsys.opba.fintech.impl.controller.utils.LoTRetrievalInformation; import de.adorsys.opba.fintech.impl.controller.utils.RestRequestContext; @@ -13,6 +14,7 @@ import de.adorsys.opba.fintech.impl.tppclients.TppAisClient; import de.adorsys.opba.tpp.ais.api.model.generated.TransactionsResponse; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -25,12 +27,14 @@ import static de.adorsys.opba.fintech.impl.tppclients.Consts.COMPUTE_FINTECH_ID; import static de.adorsys.opba.fintech.impl.tppclients.Consts.COMPUTE_X_REQUEST_SIGNATURE; import static de.adorsys.opba.fintech.impl.tppclients.Consts.COMPUTE_X_TIMESTAMP_UTC; +import static de.adorsys.opba.fintech.impl.tppclients.Consts.HEADER_COMPUTE_PSU_IP_ADDRESS; @Service @Slf4j @RequiredArgsConstructor public class TransactionService { + private final FintechUiConfig uiConfig; private final TppAisClient tppAisClient; private final RestRequestContext restRequestContext; @@ -38,24 +42,27 @@ public class TransactionService { private final RedirectHandlerService redirectHandlerService; private final ConsentRepository consentRepository; private final HandleAcceptedService handleAcceptedService; + private final ObjectMapper mapper; - public ResponseEntity listTransactions(SessionEntity sessionEntity, String fintechOkUrl, String fintechNOkUrl, String bankId, - String accountId, LocalDate dateFrom, LocalDate dateTo, String entryReferenceFrom, + @SuppressWarnings("checkstyle:MethodLength") // FIXME - It is just too many lines of text + @SneakyThrows + public ResponseEntity listTransactions(SessionEntity sessionEntity, String fintechOkUrl, String fintechNOkUrl, String bankProfileID, + String accountId, String createConsentIfNone, LocalDate dateFrom, LocalDate dateTo, String entryReferenceFrom, String bookingStatus, Boolean deltaList, LoTRetrievalInformation loTRetrievalInformation, - Boolean online) { + Boolean psuAuthenticationRequired, Boolean online) { log.info("LoT {}", loTRetrievalInformation); String fintechRedirectCode = UUID.randomUUID().toString(); Optional optionalConsent = Optional.empty(); if (loTRetrievalInformation.equals(LoTRetrievalInformation.FROM_TPP_WITH_AVAILABLE_CONSENT)) { optionalConsent = consentRepository.findFirstByUserEntityAndBankIdAndConsentTypeAndConsentConfirmedOrderByCreationTimeDesc(sessionEntity.getUserEntity(), - bankId, ConsentType.AIS, Boolean.TRUE); + bankProfileID, ConsentType.AIS, Boolean.TRUE); } if (optionalConsent.isPresent()) { log.info("LoT found valid {} consent for user {} bank {} from {}", optionalConsent.get().getConsentType(), optionalConsent.get().getUserEntity().getLoginUserName(), optionalConsent.get().getBankId(), optionalConsent.get().getCreationTime()); } else { - log.info("LoT no valid ais consent for user {} bank {} available", sessionEntity.getUserEntity().getLoginUserName(), bankId); + log.info("LoT no valid ais consent for user {} bank {} available", sessionEntity.getUserEntity().getLoginUserName(), bankProfileID); } ResponseEntity transactions = tppAisClient.getTransactions( accountId, tppProperties.getServiceSessionPassword(), @@ -65,16 +72,31 @@ public ResponseEntity listTransactions(SessionEntity sessionEntity, String finte UUID.fromString(restRequestContext.getRequestId()), COMPUTE_X_TIMESTAMP_UTC, COMPUTE_X_REQUEST_SIGNATURE, - COMPUTE_FINTECH_ID, bankId, + COMPUTE_FINTECH_ID, + UUID.fromString(bankProfileID), + psuAuthenticationRequired, optionalConsent.map(ConsentEntity::getTppServiceSessionId).orElse(null), - dateFrom, dateTo, entryReferenceFrom, bookingStatus, deltaList, online); + createConsentIfNone, + null, + HEADER_COMPUTE_PSU_IP_ADDRESS, + null, + dateFrom, + dateTo, + entryReferenceFrom, + bookingStatus, + deltaList, + online, + true, + null, + null + ); switch (transactions.getStatusCode()) { case OK: return new ResponseEntity<>(ManualMapper.fromTppToFintech(transactions.getBody()), HttpStatus.OK); case ACCEPTED: log.info("create redirect entity for lot for redirectcode {}", fintechRedirectCode); redirectHandlerService.registerRedirectStateForSession(fintechRedirectCode, fintechOkUrl, fintechNOkUrl); - return handleAcceptedService.handleAccepted(consentRepository, ConsentType.AIS, bankId, fintechRedirectCode, sessionEntity, transactions.getHeaders()); + return handleAcceptedService.handleAccepted(consentRepository, ConsentType.AIS, bankProfileID, fintechRedirectCode, sessionEntity, transactions.getHeaders()); case UNAUTHORIZED: return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); default: diff --git a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/tppclients/Consts.java b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/tppclients/Consts.java index 81650b90f1..a6992b2be3 100644 --- a/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/tppclients/Consts.java +++ b/fintech-examples/fintech-impl/src/main/java/de/adorsys/opba/fintech/impl/tppclients/Consts.java @@ -8,6 +8,7 @@ public class Consts { public static final String HEADER_SESSION_MAX_AGE = "X-SESSION-MAX-AGE"; public static final String HEADER_REDIRECT_MAX_AGE = "X-REDIRECT-MAX-AGE"; public static final String HEADER_X_REQUEST_ID = "X-REQUEST-ID"; + public static final Boolean HEADER_COMPUTE_PSU_IP_ADDRESS = true; // Actual values are set in feign request interceptor (FeignConfig.java) public static final String COMPUTE_X_TIMESTAMP_UTC = null; diff --git a/fintech-examples/fintech-impl/src/test/java/de/adorsys/opba/fintech/impl/service/BankSearchServiceTest.java b/fintech-examples/fintech-impl/src/test/java/de/adorsys/opba/fintech/impl/service/BankSearchServiceTest.java index 190f035dfd..427292a530 100644 --- a/fintech-examples/fintech-impl/src/test/java/de/adorsys/opba/fintech/impl/service/BankSearchServiceTest.java +++ b/fintech-examples/fintech-impl/src/test/java/de/adorsys/opba/fintech/impl/service/BankSearchServiceTest.java @@ -40,12 +40,13 @@ void searchBank_nonExistingBank() { when(restRequestContext.getRequestId()).thenReturn(UUID.randomUUID().toString()); when(tppBankSearchClient.bankSearchGET( any(), - eq(INVALID_KEYWORD), any(), any(), any(), + eq(INVALID_KEYWORD), eq(start), - eq(max))).thenReturn(ResponseEntity.ok().body(bankSearchResponse)); + eq(max), + any())).thenReturn(ResponseEntity.ok().body(bankSearchResponse)); // When InlineResponse2001 actual = bankSearchService.searchBank(INVALID_KEYWORD, start, max); diff --git a/fintech-examples/fintech-last-module-codecoverage/pom.xml b/fintech-examples/fintech-last-module-codecoverage/pom.xml index 422c22dbd7..1b2c7f9203 100644 --- a/fintech-examples/fintech-last-module-codecoverage/pom.xml +++ b/fintech-examples/fintech-last-module-codecoverage/pom.xml @@ -5,7 +5,7 @@ de.adorsys.opba fintech-examples - 0.30.0.1 + 1.0.0 4.0.0 diff --git a/fintech-examples/fintech-server/Dockerfile b/fintech-examples/fintech-server/Dockerfile index 2918f4d3f8..d84f117f30 100644 --- a/fintech-examples/fintech-server/Dockerfile +++ b/fintech-examples/fintech-server/Dockerfile @@ -3,7 +3,7 @@ FROM adoptopenjdk/openjdk11:jre-11.0.9_11-alpine ENV APP_HOME /usr/app WORKDIR $APP_HOME -COPY target/*.jar . +COPY target/*exec.jar . EXPOSE 8086 diff --git a/fintech-examples/fintech-server/pom.xml b/fintech-examples/fintech-server/pom.xml index a626d58a92..816cfc22c1 100644 --- a/fintech-examples/fintech-server/pom.xml +++ b/fintech-examples/fintech-server/pom.xml @@ -5,7 +5,7 @@ de.adorsys.opba fintech-examples - 0.30.0.1 + 1.0.0 fintech-server @@ -123,6 +123,8 @@ ${spring-boot.version} de.adorsys.opba.fintech.server.FinTechServer + false + exec diff --git a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechApiBaseTest.java b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechApiBaseTest.java index 7033bf8090..918ba5d66e 100644 --- a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechApiBaseTest.java +++ b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechApiBaseTest.java @@ -43,7 +43,7 @@ protected String getFilenameBankSearch(String keyword, Integer start, Integer ma + POSTFIX; } - protected String getFilenameBankProfile(String bankUUID) { + protected String getFilenameBankProfile(UUID bankUUID) { return BANK_PROFILE_RESPONSE_PREFIX + "-" + bankUUID + POSTFIX; diff --git a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechBankSearchApiTest.java b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechBankSearchApiTest.java index 598b92aebe..0e1594f16c 100644 --- a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechBankSearchApiTest.java +++ b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechBankSearchApiTest.java @@ -122,7 +122,7 @@ public void bankSearchAuthorized() { final Integer start = 1; final Integer max = 2; - when(tppBankSearchClientFeignMock.bankSearchGET(any(), eq(keyword), any(), any(), any(), eq(start), eq(max))) + when(tppBankSearchClientFeignMock.bankSearchGET(any(), any(), any(), any(), eq(keyword), eq(start), eq(max), any())) .thenReturn(ResponseEntity.ok(GSON.fromJson(readFile(getFilenameBankSearch(keyword, start, max)), BankSearchResponse.class))); LoginBody loginBody = new LoginBody("peter", "1234"); @@ -156,7 +156,7 @@ public void bankProfileAuthorized() { @NoArgsConstructor static class BankProfileTestResult { String sessionCookieValue = null; - String bankUUID = null; + UUID bankUUID = null; List services = null; } @@ -177,7 +177,7 @@ BankProfileTestResult getBankProfileTestResult() { final Integer max = 2; log.info("DO Bank Search ({}, {}, {}) ==============================", keyword, start, max); - when(tppBankSearchClientFeignMock.bankSearchGET(any(), eq(keyword), any(), any(), any(), eq(start), eq(max))) + when(tppBankSearchClientFeignMock.bankSearchGET(any(), any(), any(), any(), eq(keyword), eq(start), eq(max), any())) .thenReturn(ResponseEntity.ok(GSON.fromJson(readFile(getFilenameBankSearch(keyword, start, max)), BankSearchResponse.class))); result.setBankUUID(bankSearchOk(keyword, start, max)); @@ -185,7 +185,7 @@ BankProfileTestResult getBankProfileTestResult() { { log.info("DO Bank Profile ({}) ============================== ", result.getBankUUID()); - when(tppBankSearchClientFeignMock.bankProfileGET(any(), eq(result.getBankUUID()), any(), any(), any())) + when(tppBankSearchClientFeignMock.bankProfileGET(any(), eq(result.getBankUUID()), any(), any(), any(), any())) .thenReturn(ResponseEntity.ok(GSON.fromJson(readFile(getFilenameBankProfile(result.getBankUUID())), BankProfileResponse.class))); result.setServices(bankProfile(result.getBankUUID())); @@ -196,16 +196,16 @@ BankProfileTestResult getBankProfileTestResult() { } /** - * @param bankUUID + * @param bankProfileUUID * @return List of Services of Bank */ @SneakyThrows - List bankProfile(String bankUUID) { + List bankProfile(UUID bankProfileUUID) { MvcResult mvcResult = this.mvc .perform(get(FIN_TECH_BANK_PROFILE_URL) .header(Consts.HEADER_X_REQUEST_ID, UUID.randomUUID().toString()) .header(Consts.HEADER_XSRF_TOKEN, restRequestContext.getXsrfTokenHeaderField()) - .param("bankId", bankUUID)) + .param("bankProfileId", bankProfileUUID.toString())) .andDo(print()) .andExpect(status().isOk()) .andReturn(); @@ -276,10 +276,10 @@ MvcResult plainLogout() { * @return first BankUUID of found list */ @SneakyThrows - String bankSearchOk(String keyword, Integer start, Integer max) { + UUID bankSearchOk(String keyword, Integer start, Integer max) { MvcResult result = plainBankSearch(keyword, start, max); assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus()); - return new JSONObject(result.getResponse().getContentAsString()).getJSONArray("bankDescriptor").getJSONObject(0).get("uuid").toString(); + return UUID.fromString(new JSONObject(result.getResponse().getContentAsString()).getJSONArray("bankDescriptor").getJSONObject(0).get("uuid").toString()); } @SneakyThrows diff --git a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechListAccountsTest.java b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechListAccountsTest.java index 4d686c7b8f..4e2cf273ab 100644 --- a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechListAccountsTest.java +++ b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechListAccountsTest.java @@ -51,7 +51,7 @@ public class FinTechListAccountsTest extends FinTechBankSearchApiTest { private static final String FIN_TECH_LIST_ACCOUNTS_URL = "/v1/ais/banks/{bank-id}/accounts"; - private static final String NO_CONSENT_BANK_ID = "aaaaaaaaa-ee6e-45f9-9163-b97320c6881a"; + private static final UUID NO_CONSENT_BANK_ID = UUID.fromString("356938ab-9561-408f-ac7a-a9089c1623b7"); private static final String USERNAME = "peter"; private static final String PASSWORD = "1234"; @@ -79,7 +79,7 @@ public void testListAccountsFor200() { @SneakyThrows List listAccountsForOk(BankProfileTestResult result) { - when(tppAisClientFeignMock.getAccounts(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + when(tppAisClientFeignMock.getAccounts(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) .thenReturn(ResponseEntity.ok(GSON.fromJson(readFile("TPP_LIST_ACCOUNTS.json"), AccountList.class))); MvcResult mvcResult = plainListAccounts(result.getBankUUID()); @@ -104,20 +104,20 @@ public void testListAccountsFor303() { .build(); BankProfileTestResult result = getBankProfileTestResult(); createConsent(null, null); - when(tppAisClientFeignMock.getTransactionsWithoutAccountId(any(), any(), any(), any(), any(), any(), any(), any(), any(), - any(), any(), any(), any(), any(), any())).thenReturn(accepted); + when(tppAisClientFeignMock.getTransactionsWithoutAccountId(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(accepted); MvcResult mvcResult = plainListAccounts(result.getBankUUID()); assertEquals(ACCEPTED.value(), mvcResult.getResponse().getStatus()); - verify(tppAisClientFeignMock).getTransactionsWithoutAccountId(any(), any(), any(), any(), any(), any(), any(), any(), - any(), any(), any(), any(), any(), any(), any()); - verify(tppAisClientFeignMock, never()).getAccounts(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); + verify(tppAisClientFeignMock).getTransactionsWithoutAccountId(any(), any(), any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); + verify(tppAisClientFeignMock, never()).getAccounts(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); } @Test @SneakyThrows public void testListAccountsFor303NoConsentSupport() { - when(tppBankSearchClientFeignMock.bankProfileGET(any(), eq(NO_CONSENT_BANK_ID), any(), any(), any())) + when(tppBankSearchClientFeignMock.bankProfileGET(any(), eq(NO_CONSENT_BANK_ID), any(), any(), any(), any())) .thenReturn(ResponseEntity.ok(GSON.fromJson(readFile(getFilenameBankProfile(NO_CONSENT_BANK_ID)), BankProfileResponse.class))); when(restRequestContext.getRequestId()).thenReturn(UUID.randomUUID().toString()); @@ -128,22 +128,22 @@ public void testListAccountsFor303NoConsentSupport() { .location(new URI("affe")) .build(); createConsent(null, null); - when(tppAisClientFeignMock.getAccounts(any(), any(), any(), any(), any(), any(), any(), any(), any(), - any(), any(), any())).thenReturn(accepted); + when(tppAisClientFeignMock.getAccounts(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any())).thenReturn(accepted); MvcResult mvcResult = plainListAccounts(NO_CONSENT_BANK_ID); assertEquals(ACCEPTED.value(), mvcResult.getResponse().getStatus()); - verify(tppAisClientFeignMock, never()).getTransactions(any(), any(), any(), any(), any(), any(), any(), any(), any(), - any(), any(), any(), any(), any(), any(), any(), any()); - verify(tppAisClientFeignMock).getAccounts(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); + verify(tppAisClientFeignMock, never()).getTransactions(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), // CPD-OFF + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); + verify(tppAisClientFeignMock).getAccounts(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); // CPD-ON } @SneakyThrows - MvcResult plainListAccounts(String bankUUID) { - log.info("bankUUID {}", bankUUID); + MvcResult plainListAccounts(UUID bankProfileUUID) { + log.info("bankProfileUUID {}", bankProfileUUID); return this.mvc - .perform(get(FIN_TECH_LIST_ACCOUNTS_URL, bankUUID) + .perform(get(FIN_TECH_LIST_ACCOUNTS_URL, bankProfileUUID) .header(Consts.HEADER_X_REQUEST_ID, restRequestContext.getRequestId()) .header(Consts.HEADER_XSRF_TOKEN, restRequestContext.getXsrfTokenHeaderField()) .header("Fintech-Redirect-URL-OK", "ok") diff --git a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechListTransactionsTest.java b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechListTransactionsTest.java index 99d95e6112..bbb8019e49 100644 --- a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechListTransactionsTest.java +++ b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/FinTechListTransactionsTest.java @@ -6,6 +6,7 @@ import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; +import org.mockito.stubbing.OngoingStubbing; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.web.servlet.MvcResult; @@ -34,9 +35,7 @@ public void testListTransactionsForOk() { BankProfileTestResult result= getBankProfileTestResult(); createConsent(UUID.randomUUID().toString(), UUID.randomUUID()); List accountIDs = listAccountsForOk(result); - when(tppAisClientFeignMock.getTransactions(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), - any(), any(), any(), any(), any(), any(), any())) - .thenReturn(ResponseEntity.ok(GSON.fromJson(readFile("TPP_LIST_TRANSACTIONS.json"), TransactionsResponse.class))); + mockTransactions().thenReturn(ResponseEntity.ok(GSON.fromJson(readFile("TPP_LIST_TRANSACTIONS.json"), TransactionsResponse.class))); List amounts = listAmounts(result.getBankUUID(), accountIDs.get(0)); assertTrue(amounts.containsAll(Arrays.asList(new String[]{"1000"}))); } @@ -52,16 +51,20 @@ public void testListTransactionsForRedirect() { BankProfileTestResult result = getBankProfileTestResult(); createConsent(UUID.randomUUID().toString(), UUID.randomUUID()); - when(tppAisClientFeignMock.getTransactions(any(), any(), any(), any(), any(), any(), any(), any(), any(), - any(), any(), any(), any(), any(), any(), any(), any())) + mockTransactions() .thenReturn(accepted); MvcResult mvcResult = plainListAmounts(result.getBankUUID(), listAccountsForOk(result).get(0)); assertEquals(HttpStatus.ACCEPTED.value(), mvcResult.getResponse().getStatus()); } + private OngoingStubbing> mockTransactions() { + return when(tppAisClientFeignMock.getTransactions(any(), any(), any(), any(), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())); + } + @SneakyThrows - List listAmounts(String bankUUID, String accountID) { - MvcResult mvcResult = plainListAmounts(bankUUID, accountID); + List listAmounts(UUID bankProfileUUID, String accountID) { + MvcResult mvcResult = plainListAmounts(bankProfileUUID, accountID); assertEquals(HttpStatus.OK.value(), mvcResult.getResponse().getStatus()); List amountList = new ArrayList<>(); @@ -74,9 +77,9 @@ List listAmounts(String bankUUID, String accountID) { return amountList; } - private MvcResult plainListAmounts(String bankUUID, String accountID) throws Exception { + private MvcResult plainListAmounts(UUID bankProfileUUID, String accountID) throws Exception { return this.mvc - .perform(get(FIN_TECH_LIST_TRANSACTIONS_URL, bankUUID, accountID) + .perform(get(FIN_TECH_LIST_TRANSACTIONS_URL, bankProfileUUID, accountID) .header(Consts.HEADER_X_REQUEST_ID, restRequestContext.getRequestId()) .header(Consts.HEADER_XSRF_TOKEN, restRequestContext.getXsrfTokenHeaderField()) .header("Fintech-Redirect-URL-OK", "ok") diff --git a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/feignmocks/TppAisClientFeignMock.java b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/feignmocks/TppAisClientFeignMock.java index c76d046dd8..73a50432c3 100644 --- a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/feignmocks/TppAisClientFeignMock.java +++ b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/feignmocks/TppAisClientFeignMock.java @@ -3,7 +3,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.adorsys.opba.fintech.impl.tppclients.TppAisClient; import de.adorsys.opba.tpp.ais.api.model.generated.AccountList; +import de.adorsys.opba.tpp.ais.api.model.generated.SessionStatusDetails; import de.adorsys.opba.tpp.ais.api.model.generated.TransactionsResponse; +import de.adorsys.opba.tpp.ais.api.model.generated.UpdateAisExternalSessionStatus; import org.springframework.http.ResponseEntity; import javax.servlet.http.HttpServletRequest; @@ -24,10 +26,15 @@ public ResponseEntity getAccounts( String xTimestampUTC, String xRequestSignature, String fintechId, - String bankID, + UUID bankProfileID, + Boolean xPsuAuthenticationRequired, UUID serviceSessionID, - Boolean useObgCache, - Boolean withBalance + String createConsentIfNone, + String importUserData, + Boolean computePsuIpAddress, + String psuIpAddress, + Boolean withBalance, + Boolean online ) { return null; } @@ -43,14 +50,22 @@ public ResponseEntity getTransactions( String xTimestampUTC, String xRequestSignature, String fintechId, - String bankID, + UUID bankProfileID, + Boolean xPsuAuthenticationRequired, UUID serviceSessionID, + String createConsentIfNone, + String importUserData, + Boolean computePsuIpAddress, + String psuIpAddress, LocalDate dateFrom, @Valid LocalDate dateTo, String entryReferenceFrom, @Valid String bookingStatus, @Valid Boolean deltaList, - Boolean online + Boolean online, + Boolean analytics, + Integer page, + Integer pageSize ) { return null; } @@ -65,17 +80,41 @@ public ResponseEntity getTransactionsWithoutAccountId( String xTimestampUTC, String xRequestSignature, String fintechID, - String bankID, + UUID bankProfileID, + Boolean xPsuAuthenticationRequired, UUID serviceSessionID, + String createConsentIfNone, + String importUserData, + Boolean computePsuIpAddress, + String psuIpAddress, LocalDate dateFrom, LocalDate dateTo, String entryReferenceFrom, String bookingStatus, - Boolean deltaList + Boolean deltaList, + Integer page, + Integer pageSize ) { return null; } + @Override + public ResponseEntity deleteConsent(UUID serviceSessionID, String serviceSessionPassword, UUID xRequestID, + String xTimestampUTC, String xRequestSignature, String fintechID, Boolean deleteAll) { + return null; + } + + @Override + public ResponseEntity getAisSessionStatus(UUID serviceSessionID, String serviceSessionPassword, + UUID xRequestID, String xTimestampUTC, String xRequestSignature, String fintechID) { + return null; + } + + @Override + public ResponseEntity updateExternalAisSession(UUID serviceSessionID, String serviceSessionPassword, UUID xRequestID, String xTimestampUTC, String xRequestSignature, String fintechID) { + return null; + } + // TODO: https://github.com/adorsys/open-banking-gateway/issues/559 @Override public Optional getObjectMapper() { diff --git a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/feignmocks/TppBankSearchClientFeignMock.java b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/feignmocks/TppBankSearchClientFeignMock.java index 07ae77ca43..0ec58add46 100644 --- a/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/feignmocks/TppBankSearchClientFeignMock.java +++ b/fintech-examples/fintech-server/src/test/java/de/adorsys/opba/fintech/server/feignmocks/TppBankSearchClientFeignMock.java @@ -16,10 +16,11 @@ public class TppBankSearchClientFeignMock implements TppBankSearchClient { @Override public ResponseEntity bankProfileGET(UUID xRequestID, - @NotNull @Valid String bankId, + @NotNull @Valid UUID bankProfileID, String xTimestampUTC, String xRequestSignature, - String fintechId) { + String fintechId, + Boolean onlyActive) { return null; } @@ -31,7 +32,8 @@ public ResponseEntity bankSearchGET( String xRequestSignature, String fintechId, @Valid Integer start, - @Valid Integer max) { + @Valid Integer max, + Boolean onlyActive) { return null; } diff --git a/fintech-examples/fintech-server/src/test/resources/TPP_BankProfileResponse-aaaaaaaaa-ee6e-45f9-9163-b97320c6881a.json b/fintech-examples/fintech-server/src/test/resources/TPP_BankProfileResponse-356938ab-9561-408f-ac7a-a9089c1623b7.json similarity index 100% rename from fintech-examples/fintech-server/src/test/resources/TPP_BankProfileResponse-aaaaaaaaa-ee6e-45f9-9163-b97320c6881a.json rename to fintech-examples/fintech-server/src/test/resources/TPP_BankProfileResponse-356938ab-9561-408f-ac7a-a9089c1623b7.json diff --git a/fintech-examples/fintech-server/src/test/resources/TPP_LIST_TRANSACTIONS.json b/fintech-examples/fintech-server/src/test/resources/TPP_LIST_TRANSACTIONS.json index f524036630..111593895c 100644 --- a/fintech-examples/fintech-server/src/test/resources/TPP_LIST_TRANSACTIONS.json +++ b/fintech-examples/fintech-server/src/test/resources/TPP_LIST_TRANSACTIONS.json @@ -112,5 +112,11 @@ "proprietaryBankTransactionCode": "string" } ] + }, + "paging": { + "page": 1, + "pageSize": 20, + "pageCount": 1, + "totalCount": 2 } } \ No newline at end of file diff --git a/fintech-examples/fintech-ui/angular.json b/fintech-examples/fintech-ui/angular.json index 8db324b56a..b1670be9ac 100644 --- a/fintech-examples/fintech-ui/angular.json +++ b/fintech-examples/fintech-ui/angular.json @@ -23,10 +23,7 @@ "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "aot": false, - "assets": [ - "src/favicon.ico", - "src/assets" - ], + "assets": ["src/favicon.ico", "src/assets"], "styles": [ "src/styles.scss", "node_modules/font-awesome/css/font-awesome.css", @@ -92,28 +89,16 @@ "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.scss" - ], + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], "scripts": [] } }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { - "tsConfig": [ - "tsconfig.app.json", - "tsconfig.spec.json", - "e2e/tsconfig.json" - ], - "exclude": [ - "**/node_modules/**", - "**/api/**" - ] + "tsConfig": ["tsconfig.app.json", "tsconfig.spec.json", "e2e/tsconfig.json"], + "exclude": ["**/node_modules/**", "**/api/**"] } }, "e2e": { diff --git a/fintech-examples/fintech-ui/package.json b/fintech-examples/fintech-ui/package.json index 7b0094b0f7..86b09df3fa 100644 --- a/fintech-examples/fintech-ui/package.json +++ b/fintech-examples/fintech-ui/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "openapi-gen": "openapi-generator generate -g typescript-angular -o src/app/api -i ../fintech-api/src/main/resources/static/fintech_api.yml", + "openapi-gen": "openapi-generator generate -g typescript-angular -o src/app/api -i ../fintech-api/src/main/resources/static/fintech_api.yml --skip-validate-spec", "start": "npm run serve", "serve": "ng serve --port=4444 --proxy-config=proxy.conf.json", "serve:dev": "ng serve --port=4444 --proxy-config=proxy-conf-dev-backend.js", diff --git a/fintech-examples/fintech-ui/src/app/api/.openapi-generator/VERSION b/fintech-examples/fintech-ui/src/app/api/.openapi-generator/VERSION index 078bf8b7dd..ecedc98d1d 100644 --- a/fintech-examples/fintech-ui/src/app/api/.openapi-generator/VERSION +++ b/fintech-examples/fintech-ui/src/app/api/.openapi-generator/VERSION @@ -1 +1 @@ -4.2.2 \ No newline at end of file +4.3.1 \ No newline at end of file diff --git a/fintech-examples/fintech-ui/src/app/api/README.md b/fintech-examples/fintech-ui/src/app/api/README.md index 6fdf5e362f..e0c9192e33 100644 --- a/fintech-examples/fintech-ui/src/app/api/README.md +++ b/fintech-examples/fintech-ui/src/app/api/README.md @@ -92,6 +92,31 @@ export function apiConfigFactory (): Configuration => { export class AppModule {} ``` +``` +// configuring providers with an authentication service that manages your access tokens +import { ApiModule, Configuration } from ''; + +@NgModule({ + imports: [ ApiModule ], + declarations: [ AppComponent ], + providers: [ + { + provide: Configuration, + useFactory: (authService: AuthService) => new Configuration( + { + basePath: environment.apiUrl, + accessToken: authService.getAccessToken.bind(authService) + } + ), + deps: [AuthService], + multi: false + } + ], + bootstrap: [ AppComponent ] +}) +export class AppModule {} +``` + ``` import { DefaultApi } from ''; diff --git a/fintech-examples/fintech-ui/src/app/api/api.module.ts b/fintech-examples/fintech-ui/src/app/api/api.module.ts index 92fa029b8a..8dad7db14b 100644 --- a/fintech-examples/fintech-ui/src/app/api/api.module.ts +++ b/fintech-examples/fintech-ui/src/app/api/api.module.ts @@ -8,22 +8,17 @@ import { FinTechAuthorizationService } from './api/finTechAuthorization.service' import { FinTechBankSearchService } from './api/finTechBankSearch.service'; import { FinTechOauth2AuthenticationService } from './api/finTechOauth2Authentication.service'; import { FintechRetrieveAllSinglePaymentsService } from './api/fintechRetrieveAllSinglePayments.service'; +import { FintechRetrieveConsentService } from './api/fintechRetrieveConsent.service'; import { FintechSinglePaymentInitiationService } from './api/fintechSinglePaymentInitiation.service'; @NgModule({ imports: [], declarations: [], exports: [], - providers: [ - FinTechAccountInformationService, - FinTechAuthorizationService, - FinTechBankSearchService, - FinTechOauth2AuthenticationService, - FintechRetrieveAllSinglePaymentsService, - FintechSinglePaymentInitiationService ] + providers: [] }) export class ApiModule { - public static forRoot(configurationFactory: () => Configuration): ModuleWithProviders { + public static forRoot(configurationFactory: () => Configuration): ModuleWithProviders { return { ngModule: ApiModule, providers: [ { provide: Configuration, useFactory: configurationFactory } ] diff --git a/fintech-examples/fintech-ui/src/app/api/api/api.ts b/fintech-examples/fintech-ui/src/app/api/api/api.ts index 06fe459d81..f15065d372 100644 --- a/fintech-examples/fintech-ui/src/app/api/api/api.ts +++ b/fintech-examples/fintech-ui/src/app/api/api/api.ts @@ -8,6 +8,8 @@ export * from './finTechOauth2Authentication.service'; import { FinTechOauth2AuthenticationService } from './finTechOauth2Authentication.service'; export * from './fintechRetrieveAllSinglePayments.service'; import { FintechRetrieveAllSinglePaymentsService } from './fintechRetrieveAllSinglePayments.service'; +export * from './fintechRetrieveConsent.service'; +import { FintechRetrieveConsentService } from './fintechRetrieveConsent.service'; export * from './fintechSinglePaymentInitiation.service'; import { FintechSinglePaymentInitiationService } from './fintechSinglePaymentInitiation.service'; -export const APIS = [FinTechAccountInformationService, FinTechAuthorizationService, FinTechBankSearchService, FinTechOauth2AuthenticationService, FintechRetrieveAllSinglePaymentsService, FintechSinglePaymentInitiationService]; +export const APIS = [FinTechAccountInformationService, FinTechAuthorizationService, FinTechBankSearchService, FinTechOauth2AuthenticationService, FintechRetrieveAllSinglePaymentsService, FintechRetrieveConsentService, FintechSinglePaymentInitiationService]; diff --git a/fintech-examples/fintech-ui/src/app/api/api/finTechAccountInformation.service.ts b/fintech-examples/fintech-ui/src/app/api/api/finTechAccountInformation.service.ts index e900915269..616c66a759 100644 --- a/fintech-examples/fintech-ui/src/app/api/api/finTechAccountInformation.service.ts +++ b/fintech-examples/fintech-ui/src/app/api/api/finTechAccountInformation.service.ts @@ -1,9 +1,9 @@ /** * Open Banking Gateway FinTech Example API - * This is a sample API that shows how to develop FinTech use cases that invoke banking APIs. #### User Agent and Cookies This Api assumes * that the PsuUserAgent (hosting the FinTechUI) is a modern web browser that stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). * that any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of a modern browser with respect to Cookies. #### SessionCookies and XSRF After a PSU is authenticated with the FinTech environment (either through the simple login interface defined here, or through an identity provider), the FinTechApi will establish a session with the FinTechUI. This is done by the mean of using a cookie called SessionCookie. This SessionCookie is protected by a corresponding xsrfToken. The response that sets a SessionCookie also carries a corresponding xsrfToken in the response header named \"X-XSRF-TOKEN\". It is the responsibility of the FinTechUI to : * parse and store this xsrfToken so that a refresh of a browser window can work. This shall be done using user agent capabilities. A web browser application might decide to store the xsrfToken in the browser localStorage, as the cookie we set are all considered persistent. * make sure that each subsequent request that is carrying the SessionCookie also carries the corresponding xsrfToken as header field (see the request path). * remove this xsrfToken from the localStorage when the corresponding SessionCookie is deleted by a server response (setting cookie value to null). The main difference between an xsrfToken and a SessionCookie is that the sessionCookie is automatically sent with each matching request. The xsrfToken must be explicitely read and sent by application. #### API- vs. UI-Redirection For simplicity, this Framework is designed to redirect to FinTechUI not to FinTechApi. #### Explicite vs. Implicite Redirection We define an \"Implicite redirection\" a case where a web browser react to 30X reponse and automatically redirects to the attached endpoint. We define an \"Explicite Redirection\" as a case where the UI-Application reacts to a 20X response, explicitely parses the attached __Location__ header an uses it to reload the new page in the browser window (or start the new UI-Application in case of native apps). This framework advocates for explicite redirection passing a __20X__ response to the FinTechUI toghether with the __Location__ parameter. Processing a response that initiates a redirect, the FinTechUI makes sure following happens, * that the exisitng __SessionCookie__ is deleted, as the user will not have a chance for an explicite logout, * that the corresponding xsrfToken is deleted from the local storage, * that a RedirectCookie set is stored (in case UI is not a web browser), so the user can be authenticated against it when sent back to the FinTechUI. The expiration of the RedirectCookie shall be set to the expected duration of the redirect, * that the corresponding xsrfToken is stored in the local storage (under the same cookie path as the RedirectCookie) #### Redirecting to the ConsentAuthorisationApi For a redirection to the ConsentAuthorisationApi, a generated AUTH-ID is added to the cookie path and used to distinguish authorization processes from each order. This information (AUTH-ID) must be contained in the back redirect url sent to the ConsentAuthorisationApi in the back channel, so that the FinTechUI can invoke the correct code2Token endpoint when activated. + * This is a sample API that shows how to develop FinTech use cases that invoke banking APIs. #### User Agent and Cookies This Api assumes * that the PsuUserAgent (hosting the FinTechUI) is a modern web browser that stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). * that any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of a modern browser with respect to Cookies. #### SessionCookies and XSRF After a PSU is authenticated with the FinTech environment (either through the simple login interface defined here, or through an identity provider), the FinTechApi will establish a session with the FinTechUI. This is done by the mean of using a cookie called SessionCookie. This SessionCookie is protected by a corresponding xsrfToken. The response that sets a SessionCookie also carries a corresponding xsrfToken in the response header named \"X-XSRF-TOKEN\". It is the responsibility of the FinTechUI to : * parse and store this xsrfToken so that a refresh of a browser window can work. This shall be done using user agent capabilities. A web browser application might decide to store the xsrfToken in the browser localStorage, as the cookie we set are all considered persistent. * make sure that each subsequent request that is carrying the SessionCookie also carries the corresponding xsrfToken as header field (see the request path). * remove this xsrfToken from the localStorage when the corresponding SessionCookie is deleted by a server response (setting cookie value to null). The main difference between an xsrfToken and a SessionCookie is that the sessionCookie is automatically sent with each matching request. The xsrfToken must be explicitely read and sent by application. #### API- vs. UI-Redirection For simplicity, this Framework is designed to redirect to FinTechUI not to FinTechApi. #### Explicite vs. Implicite Redirection We define an \"Implicite redirection\" a case where a web browser react to 30X reponse and automatically redirects to the attached endpoint. We define an \"Explicite Redirection\" as a case where the UI-Application reacts to a 20X response, explicitely parses the attached __Location__ header an uses it to reload the new page in the browser window (or start the new UI-Application in case of native apps). This framework advocates for explicite redirection passing a __20X__ response to the FinTechUI toghether with the __Location__ parameter. Processing a response that initiates a redirect, the FinTechUI makes sure following happens, * that the exisitng __SessionCookie__ is deleted, as the user will not have a chance for an explicite logout, * that the corresponding xsrfToken is deleted from the local storage, * that a RedirectCookie set is stored (in case UI is not a web browser), so the user can be authenticated against it when sent back to the FinTechUI. The expiration of the RedirectCookie shall be set to the expected duration of the redirect, * that the corresponding xsrfToken is stored in the local storage (under the same cookie path as the RedirectCookie) #### Redirecting to the ConsentAuthorisationApi For a redirection to the ConsentAuthorisationApi, a generated AUTH-ID is added to the cookie path and used to distinguish authorization processes from each order. This information (AUTH-ID) must be contained in the back redirect url sent to the ConsentAuthorisationApi in the back channel, so that the FinTechUI can invoke the correct code2Token endpoint when activated. * * The version of the OpenAPI document: 1.0.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -17,10 +17,10 @@ import { HttpClient, HttpHeaders, HttpParams, import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; -import { AccountList } from '../model/accountList'; -import { ErrorResponse } from '../model/errorResponse'; -import { PsuMessage } from '../model/psuMessage'; -import { TransactionsResponse } from '../model/transactionsResponse'; +import { AccountList } from '../model/models'; +import { ErrorResponse } from '../model/models'; +import { PsuMessage } from '../model/models'; +import { TransactionsResponse } from '../model/models'; import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; import { Configuration } from '../configuration'; @@ -52,32 +52,70 @@ export class FinTechAccountInformationService { + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, + (value as Date).toISOString().substr(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + /** * Provides list of available accounts for the given bank - * Read the identifiers of the available payment accounts. If required by the bank, PSU consent will be obtained before returning the list of bank accounts. Returns all identifiers of the accounts, to which an account access has been granted to by the PSU. In addition, relevant information about the accounts and hyperlinks to corresponding account information resources are provided if a related consent has been already granted. - * @param bankId - * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. - * @param fintechRedirectURLOK - * @param fintechRedirectURLNOK - * @param loARetrievalInformation + * Read the identifiers of the available payment accounts. If required by the bank, PSU consent will be obtained before returning the list of bank accounts. Returns all identifiers of the accounts, to which an account access has been granted to by the PSU. In addition, relevant information about the accounts and hyperlinks to corresponding account information resources are provided if a related consent has been already granted. + * @param bankId + * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. + * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. + * @param fintechRedirectURLOK + * @param fintechRedirectURLNOK + * @param loARetrievalInformation + * @param xPsuAuthenticationRequired If false, login form to OPBA will not be displayed as there might be nothing to share for payments, so that authentication is not necessary. If absent or true - login form for payments will be displayed. + * @param xCreateConsentIfNone * @param withBalance Provides balances for the given accounts - * @param online If false, new data will be requested and cache will be updated + * @param online If true, new data will be requested and cache will be updated * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public aisAccountsGET(bankId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loARetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', withBalance?: boolean, online?: boolean, observe?: 'body', reportProgress?: boolean): Observable; - public aisAccountsGET(bankId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loARetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', withBalance?: boolean, online?: boolean, observe?: 'response', reportProgress?: boolean): Observable>; - public aisAccountsGET(bankId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loARetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', withBalance?: boolean, online?: boolean, observe?: 'events', reportProgress?: boolean): Observable>; - public aisAccountsGET(bankId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loARetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', withBalance?: boolean, online?: boolean, observe: any = 'body', reportProgress: boolean = false ): Observable { + public aisAccountsGET(bankId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loARetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', xPsuAuthenticationRequired?: boolean, xCreateConsentIfNone?: string, withBalance?: boolean, online?: boolean, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public aisAccountsGET(bankId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loARetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', xPsuAuthenticationRequired?: boolean, xCreateConsentIfNone?: string, withBalance?: boolean, online?: boolean, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public aisAccountsGET(bankId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loARetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', xPsuAuthenticationRequired?: boolean, xCreateConsentIfNone?: string, withBalance?: boolean, online?: boolean, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public aisAccountsGET(bankId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loARetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', xPsuAuthenticationRequired?: boolean, xCreateConsentIfNone?: string, withBalance?: boolean, online?: boolean, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (bankId === null || bankId === undefined) { throw new Error('Required parameter bankId was null or undefined when calling aisAccountsGET.'); } if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling aisAccountsGET.'); } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling aisAccountsGET.'); + if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { + throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling aisAccountsGET.'); } if (fintechRedirectURLOK === null || fintechRedirectURLOK === undefined) { throw new Error('Required parameter fintechRedirectURLOK was null or undefined when calling aisAccountsGET.'); @@ -91,18 +129,23 @@ export class FinTechAccountInformationService { let queryParameters = new HttpParams({encoder: this.encoder}); if (withBalance !== undefined && withBalance !== null) { - queryParameters = queryParameters.set('withBalance', withBalance); + queryParameters = this.addToHttpParams(queryParameters, + withBalance, 'withBalance'); } if (online !== undefined && online !== null) { - queryParameters = queryParameters.set('online', online); + queryParameters = this.addToHttpParams(queryParameters, + online, 'online'); } let headers = this.defaultHeaders; if (xRequestID !== undefined && xRequestID !== null) { headers = headers.set('X-Request-ID', String(xRequestID)); } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); + if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { + headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); + } + if (xPsuAuthenticationRequired !== undefined && xPsuAuthenticationRequired !== null) { + headers = headers.set('X-Psu-Authentication-Required', String(xPsuAuthenticationRequired)); } if (fintechRedirectURLOK !== undefined && fintechRedirectURLOK !== null) { headers = headers.set('Fintech-Redirect-URL-OK', String(fintechRedirectURLOK)); @@ -113,21 +156,106 @@ export class FinTechAccountInformationService { if (loARetrievalInformation !== undefined && loARetrievalInformation !== null) { headers = headers.set('LoARetrievalInformation', String(loARetrievalInformation)); } + if (xCreateConsentIfNone !== undefined && xCreateConsentIfNone !== null) { + headers = headers.set('X-Create-Consent-If-None', String(xCreateConsentIfNone)); + } // authentication (sessionCookie) required - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + if (this.configuration.apiKeys) { + const key: string | undefined = this.configuration.apiKeys["sessionCookie"] || this.configuration.apiKeys["sessionCookie"]; + if (key) { + } + } + + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.get(`${this.configuration.basePath}/v1/ais/banks/${encodeURIComponent(String(bankId))}/accounts`, { params: queryParameters, + responseType: responseType, + withCredentials: this.configuration.withCredentials, + headers: headers, + observe: observe, + reportProgress: reportProgress + } + ); + } + + /** + * Deletes all consents that are associated with bank + * Deletes all consents that are associated with bank + * @param bankId + * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. + * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public aisConsentsDELETE(bankId: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public aisConsentsDELETE(bankId: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public aisConsentsDELETE(bankId: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public aisConsentsDELETE(bankId: string, xRequestID: string, xXSRFTOKEN: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { + if (bankId === null || bankId === undefined) { + throw new Error('Required parameter bankId was null or undefined when calling aisConsentsDELETE.'); + } + if (xRequestID === null || xRequestID === undefined) { + throw new Error('Required parameter xRequestID was null or undefined when calling aisConsentsDELETE.'); + } + if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { + throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling aisConsentsDELETE.'); + } + + let headers = this.defaultHeaders; + if (xRequestID !== undefined && xRequestID !== null) { + headers = headers.set('X-Request-ID', String(xRequestID)); + } + if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { + headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); + } + + // authentication (sessionCookie) required + if (this.configuration.apiKeys) { + const key: string | undefined = this.configuration.apiKeys["sessionCookie"] || this.configuration.apiKeys["sessionCookie"]; + if (key) { + } + } + + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (httpHeaderAcceptSelected !== undefined) { + headers = headers.set('Accept', httpHeaderAcceptSelected); + } + + + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + + return this.httpClient.delete(`${this.configuration.basePath}/v1/ais/banks/${encodeURIComponent(String(bankId))}/consents`, + { + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, @@ -139,26 +267,28 @@ export class FinTechAccountInformationService { /** * Returns the list of transactions of the given account * Returns the list of transactions of the given account. - * @param bankId - * @param accountId - * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. - * @param fintechRedirectURLOK - * @param fintechRedirectURLNOK - * @param loTRetrievalInformation - * @param dateFrom Conditional: Starting date (inclusive the date dateFrom) of the transaction list, mandated if no delta access is required. For booked transactions, the relevant date is the booking date. For pending transactions, the relevant date is the entry date, which may not be transparent neither in this API nor other channels of the ASPSP. - * @param dateTo End date (inclusive the data dateTo) of the transaction list, default is \"now\" if not given. Might be ignored if a delta function is used. For booked transactions, the relevant date is the booking date. For pending transactions, the relevant date is the entry date, which may not be transparent neither in this API nor other channels of the ASPSP. - * @param entryReferenceFrom This data attribute is indicating that the AISP is in favour to get all transactions after the transaction with identification entryReferenceFrom alternatively to the above defined period. This is a implementation of a delta access. If this data element is contained, the entries \"dateFrom\" and \"dateTo\" might be ignored by the ASPSP if a delta report is supported. Optional if supported by API provider. - * @param bookingStatus Permitted codes are * \"booked\", * \"pending\" and * \"both\" To support the \"pending\" and \"both\" feature is optional for the ASPSP, Error code if not supported in the online banking frontend Default is \"booked\" - * @param deltaList This data attribute is indicating that the AISP is in favour to get all transactions after the last report access for this PSU on the addressed account. This is another implementation of a delta access-report. This delta indicator might be rejected by the ASPSP if this function is not supported. Optional if supported by API provider - * @param online If false, new data will be requested and cache will be updated + * @param bankId + * @param accountId + * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. + * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. + * @param fintechRedirectURLOK + * @param fintechRedirectURLNOK + * @param loTRetrievalInformation + * @param xPsuAuthenticationRequired If false, login form to OPBA will not be displayed as there might be nothing to share for payments, so that authentication is not necessary. If absent or true - login form for payments will be displayed. + * @param xCreateConsentIfNone + * @param dateFrom Conditional: Starting date (inclusive the date dateFrom) of the transaction list, mandated if no delta access is required. For booked transactions, the relevant date is the booking date. For pending transactions, the relevant date is the entry date, which may not be transparent neither in this API nor other channels of the ASPSP. + * @param dateTo End date (inclusive the data dateTo) of the transaction list, default is \"now\" if not given. Might be ignored if a delta function is used. For booked transactions, the relevant date is the booking date. For pending transactions, the relevant date is the entry date, which may not be transparent neither in this API nor other channels of the ASPSP. + * @param entryReferenceFrom This data attribute is indicating that the AISP is in favour to get all transactions after the transaction with identification entryReferenceFrom alternatively to the above defined period. This is a implementation of a delta access. If this data element is contained, the entries \"dateFrom\" and \"dateTo\" might be ignored by the ASPSP if a delta report is supported. Optional if supported by API provider. + * @param bookingStatus Permitted codes are * \"booked\", * \"pending\" and * \"both\" To support the \"pending\" and \"both\" feature is optional for the ASPSP, Error code if not supported in the online banking frontend Default is \"booked\" + * @param deltaList This data attribute is indicating that the AISP is in favour to get all transactions after the last report access for this PSU on the addressed account. This is another implementation of a delta access-report. This delta indicator might be rejected by the ASPSP if this function is not supported. Optional if supported by API provider + * @param online If true, new data will be requested and cache will be updated * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public aisTransactionsGET(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loTRetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', dateFrom?: string, dateTo?: string, entryReferenceFrom?: string, bookingStatus?: 'booked' | 'pending' | 'both', deltaList?: boolean, online?: boolean, observe?: 'body', reportProgress?: boolean): Observable; - public aisTransactionsGET(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loTRetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', dateFrom?: string, dateTo?: string, entryReferenceFrom?: string, bookingStatus?: 'booked' | 'pending' | 'both', deltaList?: boolean, online?: boolean, observe?: 'response', reportProgress?: boolean): Observable>; - public aisTransactionsGET(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loTRetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', dateFrom?: string, dateTo?: string, entryReferenceFrom?: string, bookingStatus?: 'booked' | 'pending' | 'both', deltaList?: boolean, online?: boolean, observe?: 'events', reportProgress?: boolean): Observable>; - public aisTransactionsGET(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loTRetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', dateFrom?: string, dateTo?: string, entryReferenceFrom?: string, bookingStatus?: 'booked' | 'pending' | 'both', deltaList?: boolean, online?: boolean, observe: any = 'body', reportProgress: boolean = false ): Observable { + public aisTransactionsGET(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loTRetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', xPsuAuthenticationRequired?: boolean, xCreateConsentIfNone?: string, dateFrom?: string, dateTo?: string, entryReferenceFrom?: string, bookingStatus?: 'booked' | 'pending' | 'both', deltaList?: boolean, online?: boolean, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public aisTransactionsGET(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loTRetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', xPsuAuthenticationRequired?: boolean, xCreateConsentIfNone?: string, dateFrom?: string, dateTo?: string, entryReferenceFrom?: string, bookingStatus?: 'booked' | 'pending' | 'both', deltaList?: boolean, online?: boolean, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public aisTransactionsGET(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loTRetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', xPsuAuthenticationRequired?: boolean, xCreateConsentIfNone?: string, dateFrom?: string, dateTo?: string, entryReferenceFrom?: string, bookingStatus?: 'booked' | 'pending' | 'both', deltaList?: boolean, online?: boolean, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public aisTransactionsGET(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, loTRetrievalInformation: 'FROM_TPP_WITH_AVAILABLE_CONSENT' | 'FROM_TPP_WITH_NEW_CONSENT', xPsuAuthenticationRequired?: boolean, xCreateConsentIfNone?: string, dateFrom?: string, dateTo?: string, entryReferenceFrom?: string, bookingStatus?: 'booked' | 'pending' | 'both', deltaList?: boolean, online?: boolean, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (bankId === null || bankId === undefined) { throw new Error('Required parameter bankId was null or undefined when calling aisTransactionsGET.'); } @@ -168,8 +298,8 @@ export class FinTechAccountInformationService { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling aisTransactionsGET.'); } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling aisTransactionsGET.'); + if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { + throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling aisTransactionsGET.'); } if (fintechRedirectURLOK === null || fintechRedirectURLOK === undefined) { throw new Error('Required parameter fintechRedirectURLOK was null or undefined when calling aisTransactionsGET.'); @@ -183,30 +313,39 @@ export class FinTechAccountInformationService { let queryParameters = new HttpParams({encoder: this.encoder}); if (dateFrom !== undefined && dateFrom !== null) { - queryParameters = queryParameters.set('dateFrom', dateFrom); + queryParameters = this.addToHttpParams(queryParameters, + dateFrom, 'dateFrom'); } if (dateTo !== undefined && dateTo !== null) { - queryParameters = queryParameters.set('dateTo', dateTo); + queryParameters = this.addToHttpParams(queryParameters, + dateTo, 'dateTo'); } if (entryReferenceFrom !== undefined && entryReferenceFrom !== null) { - queryParameters = queryParameters.set('entryReferenceFrom', entryReferenceFrom); + queryParameters = this.addToHttpParams(queryParameters, + entryReferenceFrom, 'entryReferenceFrom'); } if (bookingStatus !== undefined && bookingStatus !== null) { - queryParameters = queryParameters.set('bookingStatus', bookingStatus); + queryParameters = this.addToHttpParams(queryParameters, + bookingStatus, 'bookingStatus'); } if (deltaList !== undefined && deltaList !== null) { - queryParameters = queryParameters.set('deltaList', deltaList); + queryParameters = this.addToHttpParams(queryParameters, + deltaList, 'deltaList'); } if (online !== undefined && online !== null) { - queryParameters = queryParameters.set('online', online); + queryParameters = this.addToHttpParams(queryParameters, + online, 'online'); } let headers = this.defaultHeaders; if (xRequestID !== undefined && xRequestID !== null) { headers = headers.set('X-Request-ID', String(xRequestID)); } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); + if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { + headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); + } + if (xPsuAuthenticationRequired !== undefined && xPsuAuthenticationRequired !== null) { + headers = headers.set('X-Psu-Authentication-Required', String(xPsuAuthenticationRequired)); } if (fintechRedirectURLOK !== undefined && fintechRedirectURLOK !== null) { headers = headers.set('Fintech-Redirect-URL-OK', String(fintechRedirectURLOK)); @@ -217,21 +356,39 @@ export class FinTechAccountInformationService { if (loTRetrievalInformation !== undefined && loTRetrievalInformation !== null) { headers = headers.set('LoTRetrievalInformation', String(loTRetrievalInformation)); } + if (xCreateConsentIfNone !== undefined && xCreateConsentIfNone !== null) { + headers = headers.set('X-Create-Consent-If-None', String(xCreateConsentIfNone)); + } // authentication (sessionCookie) required - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + if (this.configuration.apiKeys) { + const key: string | undefined = this.configuration.apiKeys["sessionCookie"] || this.configuration.apiKeys["sessionCookie"]; + if (key) { + } + } + + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.get(`${this.configuration.basePath}/v1/ais/banks/${encodeURIComponent(String(bankId))}/accounts/${encodeURIComponent(String(accountId))}/transactions`, { params: queryParameters, + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, diff --git a/fintech-examples/fintech-ui/src/app/api/api/finTechAuthorization.service.ts b/fintech-examples/fintech-ui/src/app/api/api/finTechAuthorization.service.ts index 0bba6b26d1..938016cc90 100644 --- a/fintech-examples/fintech-ui/src/app/api/api/finTechAuthorization.service.ts +++ b/fintech-examples/fintech-ui/src/app/api/api/finTechAuthorization.service.ts @@ -17,10 +17,10 @@ import { HttpClient, HttpHeaders, HttpParams, import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; -import { ErrorResponse } from '../model/errorResponse'; -import { InlineResponse200 } from '../model/inlineResponse200'; -import { LoginRequest } from '../model/loginRequest'; -import { PsuMessage } from '../model/psuMessage'; +import { ErrorResponse } from '../model/models'; +import { InlineResponse200 } from '../model/models'; +import { LoginRequest } from '../model/models'; +import { PsuMessage } from '../model/models'; import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; import { Configuration } from '../configuration'; @@ -52,6 +52,42 @@ export class FinTechAuthorizationService { + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, + (value as Date).toISOString().substr(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + /** * Oauth2 callback to identify user. * Oauth2 callback to authenticate user using some Oauth2 identity provider account. Provider id is set inside state. @@ -62,10 +98,10 @@ export class FinTechAuthorizationService { * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public callbackGetLogin(code: string, state: string, scope: string, error?: string, observe?: 'body', reportProgress?: boolean): Observable; - public callbackGetLogin(code: string, state: string, scope: string, error?: string, observe?: 'response', reportProgress?: boolean): Observable>; - public callbackGetLogin(code: string, state: string, scope: string, error?: string, observe?: 'events', reportProgress?: boolean): Observable>; - public callbackGetLogin(code: string, state: string, scope: string, error?: string, observe: any = 'body', reportProgress: boolean = false ): Observable { + public callbackGetLogin(code: string, state: string, scope: string, error?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public callbackGetLogin(code: string, state: string, scope: string, error?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public callbackGetLogin(code: string, state: string, scope: string, error?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public callbackGetLogin(code: string, state: string, scope: string, error?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (code === null || code === undefined) { throw new Error('Required parameter code was null or undefined when calling callbackGetLogin.'); } @@ -78,33 +114,46 @@ export class FinTechAuthorizationService { let queryParameters = new HttpParams({encoder: this.encoder}); if (code !== undefined && code !== null) { - queryParameters = queryParameters.set('code', code); + queryParameters = this.addToHttpParams(queryParameters, + code, 'code'); } if (state !== undefined && state !== null) { - queryParameters = queryParameters.set('state', state); + queryParameters = this.addToHttpParams(queryParameters, + state, 'state'); } if (scope !== undefined && scope !== null) { - queryParameters = queryParameters.set('scope', scope); + queryParameters = this.addToHttpParams(queryParameters, + scope, 'scope'); } if (error !== undefined && error !== null) { - queryParameters = queryParameters.set('error', error); + queryParameters = this.addToHttpParams(queryParameters, + error, 'error'); } let headers = this.defaultHeaders; - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.get(`${this.configuration.basePath}/v1/login/oauth2`, { params: queryParameters, + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, @@ -120,14 +169,14 @@ export class FinTechAuthorizationService { * @param okOrNotok * @param redirectCode * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. + * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public fromConsentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, X_XSRF_TOKEN: string, observe?: 'body', reportProgress?: boolean): Observable; - public fromConsentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, X_XSRF_TOKEN: string, observe?: 'response', reportProgress?: boolean): Observable>; - public fromConsentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, X_XSRF_TOKEN: string, observe?: 'events', reportProgress?: boolean): Observable>; - public fromConsentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, X_XSRF_TOKEN: string, observe: any = 'body', reportProgress: boolean = false ): Observable { + public fromConsentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public fromConsentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public fromConsentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public fromConsentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, xXSRFTOKEN: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (authId === null || authId === undefined) { throw new Error('Required parameter authId was null or undefined when calling fromConsentGET.'); } @@ -140,36 +189,46 @@ export class FinTechAuthorizationService { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling fromConsentGET.'); } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling fromConsentGET.'); + if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { + throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling fromConsentGET.'); } let queryParameters = new HttpParams({encoder: this.encoder}); if (redirectCode !== undefined && redirectCode !== null) { - queryParameters = queryParameters.set('redirectCode', redirectCode); + queryParameters = this.addToHttpParams(queryParameters, + redirectCode, 'redirectCode'); } let headers = this.defaultHeaders; if (xRequestID !== undefined && xRequestID !== null) { headers = headers.set('X-Request-ID', String(xRequestID)); } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); + if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { + headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); } - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.get(`${this.configuration.basePath}/v1/${encodeURIComponent(String(authId))}/fromConsent/${encodeURIComponent(String(okOrNotok))}`, { params: queryParameters, + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, @@ -185,14 +244,14 @@ export class FinTechAuthorizationService { * @param okOrNotok * @param redirectCode * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. + * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public fromPaymentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, X_XSRF_TOKEN: string, observe?: 'body', reportProgress?: boolean): Observable; - public fromPaymentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, X_XSRF_TOKEN: string, observe?: 'response', reportProgress?: boolean): Observable>; - public fromPaymentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, X_XSRF_TOKEN: string, observe?: 'events', reportProgress?: boolean): Observable>; - public fromPaymentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, X_XSRF_TOKEN: string, observe: any = 'body', reportProgress: boolean = false ): Observable { + public fromPaymentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public fromPaymentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public fromPaymentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public fromPaymentGET(authId: string, okOrNotok: 'OK' | 'NOT_OK', redirectCode: string, xRequestID: string, xXSRFTOKEN: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (authId === null || authId === undefined) { throw new Error('Required parameter authId was null or undefined when calling fromPaymentGET.'); } @@ -205,36 +264,46 @@ export class FinTechAuthorizationService { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling fromPaymentGET.'); } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling fromPaymentGET.'); + if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { + throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling fromPaymentGET.'); } let queryParameters = new HttpParams({encoder: this.encoder}); if (redirectCode !== undefined && redirectCode !== null) { - queryParameters = queryParameters.set('redirectCode', redirectCode); + queryParameters = this.addToHttpParams(queryParameters, + redirectCode, 'redirectCode'); } let headers = this.defaultHeaders; if (xRequestID !== undefined && xRequestID !== null) { headers = headers.set('X-Request-ID', String(xRequestID)); } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); + if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { + headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); } - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.get(`${this.configuration.basePath}/v1/${encodeURIComponent(String(authId))}/fromPayment/${encodeURIComponent(String(okOrNotok))}`, { params: queryParameters, + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, @@ -251,10 +320,10 @@ export class FinTechAuthorizationService { * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public loginPOST(xRequestID: string, loginRequest: LoginRequest, observe?: 'body', reportProgress?: boolean): Observable; - public loginPOST(xRequestID: string, loginRequest: LoginRequest, observe?: 'response', reportProgress?: boolean): Observable>; - public loginPOST(xRequestID: string, loginRequest: LoginRequest, observe?: 'events', reportProgress?: boolean): Observable>; - public loginPOST(xRequestID: string, loginRequest: LoginRequest, observe: any = 'body', reportProgress: boolean = false ): Observable { + public loginPOST(xRequestID: string, loginRequest: LoginRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public loginPOST(xRequestID: string, loginRequest: LoginRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public loginPOST(xRequestID: string, loginRequest: LoginRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public loginPOST(xRequestID: string, loginRequest: LoginRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling loginPOST.'); } @@ -267,11 +336,14 @@ export class FinTechAuthorizationService { headers = headers.set('X-Request-ID', String(xRequestID)); } - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } @@ -286,9 +358,15 @@ export class FinTechAuthorizationService { headers = headers.set('Content-Type', httpContentTypeSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.post(`${this.configuration.basePath}/v1/login`, loginRequest, { + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, @@ -301,43 +379,58 @@ export class FinTechAuthorizationService { * logs out user * If user can be authenticated, user will be logged out. * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. + * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public logoutPOST(xRequestID: string, X_XSRF_TOKEN: string, observe?: 'body', reportProgress?: boolean): Observable; - public logoutPOST(xRequestID: string, X_XSRF_TOKEN: string, observe?: 'response', reportProgress?: boolean): Observable>; - public logoutPOST(xRequestID: string, X_XSRF_TOKEN: string, observe?: 'events', reportProgress?: boolean): Observable>; - public logoutPOST(xRequestID: string, X_XSRF_TOKEN: string, observe: any = 'body', reportProgress: boolean = false ): Observable { + public logoutPOST(xRequestID: string, xXSRFTOKEN: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public logoutPOST(xRequestID: string, xXSRFTOKEN: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public logoutPOST(xRequestID: string, xXSRFTOKEN: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public logoutPOST(xRequestID: string, xXSRFTOKEN: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling logoutPOST.'); } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling logoutPOST.'); + if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { + throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling logoutPOST.'); } let headers = this.defaultHeaders; if (xRequestID !== undefined && xRequestID !== null) { headers = headers.set('X-Request-ID', String(xRequestID)); } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); + if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { + headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); } // authentication (sessionCookie) required - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + if (this.configuration.apiKeys) { + const key: string | undefined = this.configuration.apiKeys["sessionCookie"] || this.configuration.apiKeys["sessionCookie"]; + if (key) { + } + } + + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.post(`${this.configuration.basePath}/v1/logout`, null, { + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, diff --git a/fintech-examples/fintech-ui/src/app/api/api/finTechBankSearch.service.ts b/fintech-examples/fintech-ui/src/app/api/api/finTechBankSearch.service.ts index cd14d0a864..e648ef0805 100644 --- a/fintech-examples/fintech-ui/src/app/api/api/finTechBankSearch.service.ts +++ b/fintech-examples/fintech-ui/src/app/api/api/finTechBankSearch.service.ts @@ -17,10 +17,10 @@ import { HttpClient, HttpHeaders, HttpParams, import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; -import { ErrorResponse } from '../model/errorResponse'; -import { InlineResponse2001 } from '../model/inlineResponse2001'; -import { InlineResponse2002 } from '../model/inlineResponse2002'; -import { PsuMessage } from '../model/psuMessage'; +import { ErrorResponse } from '../model/models'; +import { InlineResponse2001 } from '../model/models'; +import { InlineResponse2002 } from '../model/models'; +import { PsuMessage } from '../model/models'; import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; import { Configuration } from '../configuration'; @@ -52,56 +52,108 @@ export class FinTechBankSearchService { + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, + (value as Date).toISOString().substr(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + /** - * Request the profile of the bank identified with id (bankId). - * Request the profile of the bank identified with id (bankId). + * Request the profile of the bank identified with id (bankProfileId). + * Request the profile of the bank identified with id (bankProfileId). * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. - * @param bankId Identifier of the bank to be loaded. + * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. + * @param bankProfileId Identifier of the bank to be loaded. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public bankProfileGET(xRequestID: string, X_XSRF_TOKEN: string, bankId: string, observe?: 'body', reportProgress?: boolean): Observable; - public bankProfileGET(xRequestID: string, X_XSRF_TOKEN: string, bankId: string, observe?: 'response', reportProgress?: boolean): Observable>; - public bankProfileGET(xRequestID: string, X_XSRF_TOKEN: string, bankId: string, observe?: 'events', reportProgress?: boolean): Observable>; - public bankProfileGET(xRequestID: string, X_XSRF_TOKEN: string, bankId: string, observe: any = 'body', reportProgress: boolean = false ): Observable { + public bankProfileGET(xRequestID: string, xXSRFTOKEN: string, bankProfileId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public bankProfileGET(xRequestID: string, xXSRFTOKEN: string, bankProfileId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public bankProfileGET(xRequestID: string, xXSRFTOKEN: string, bankProfileId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public bankProfileGET(xRequestID: string, xXSRFTOKEN: string, bankProfileId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling bankProfileGET.'); } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling bankProfileGET.'); + if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { + throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling bankProfileGET.'); } - if (bankId === null || bankId === undefined) { - throw new Error('Required parameter bankId was null or undefined when calling bankProfileGET.'); + if (bankProfileId === null || bankProfileId === undefined) { + throw new Error('Required parameter bankProfileId was null or undefined when calling bankProfileGET.'); } let queryParameters = new HttpParams({encoder: this.encoder}); - if (bankId !== undefined && bankId !== null) { - queryParameters = queryParameters.set('bankId', bankId); + if (bankProfileId !== undefined && bankProfileId !== null) { + queryParameters = this.addToHttpParams(queryParameters, + bankProfileId, 'bankProfileId'); } let headers = this.defaultHeaders; if (xRequestID !== undefined && xRequestID !== null) { headers = headers.set('X-Request-ID', String(xRequestID)); } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); + if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { + headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); } // authentication (sessionCookie) required - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + if (this.configuration.apiKeys) { + const key: string | undefined = this.configuration.apiKeys["sessionCookie"] || this.configuration.apiKeys["sessionCookie"]; + if (key) { + } + } + + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.get(`${this.configuration.basePath}/v1/search/bankProfile`, { params: queryParameters, + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, @@ -114,22 +166,22 @@ export class FinTechBankSearchService { * Issues an incremental bank search request to the FinTechApi. * Issues an incremental bank search request to the FinTechApi. * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. + * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. * @param keyword * @param start * @param max * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public bankSearchGET(xRequestID: string, X_XSRF_TOKEN: string, keyword: string, start?: number, max?: number, observe?: 'body', reportProgress?: boolean): Observable; - public bankSearchGET(xRequestID: string, X_XSRF_TOKEN: string, keyword: string, start?: number, max?: number, observe?: 'response', reportProgress?: boolean): Observable>; - public bankSearchGET(xRequestID: string, X_XSRF_TOKEN: string, keyword: string, start?: number, max?: number, observe?: 'events', reportProgress?: boolean): Observable>; - public bankSearchGET(xRequestID: string, X_XSRF_TOKEN: string, keyword: string, start?: number, max?: number, observe: any = 'body', reportProgress: boolean = false ): Observable { + public bankSearchGET(xRequestID: string, xXSRFTOKEN: string, keyword: string, start?: number, max?: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public bankSearchGET(xRequestID: string, xXSRFTOKEN: string, keyword: string, start?: number, max?: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public bankSearchGET(xRequestID: string, xXSRFTOKEN: string, keyword: string, start?: number, max?: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public bankSearchGET(xRequestID: string, xXSRFTOKEN: string, keyword: string, start?: number, max?: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling bankSearchGET.'); } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling bankSearchGET.'); + if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { + throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling bankSearchGET.'); } if (keyword === null || keyword === undefined) { throw new Error('Required parameter keyword was null or undefined when calling bankSearchGET.'); @@ -137,37 +189,55 @@ export class FinTechBankSearchService { let queryParameters = new HttpParams({encoder: this.encoder}); if (keyword !== undefined && keyword !== null) { - queryParameters = queryParameters.set('keyword', keyword); + queryParameters = this.addToHttpParams(queryParameters, + keyword, 'keyword'); } if (start !== undefined && start !== null) { - queryParameters = queryParameters.set('start', start); + queryParameters = this.addToHttpParams(queryParameters, + start, 'start'); } if (max !== undefined && max !== null) { - queryParameters = queryParameters.set('max', max); + queryParameters = this.addToHttpParams(queryParameters, + max, 'max'); } let headers = this.defaultHeaders; if (xRequestID !== undefined && xRequestID !== null) { headers = headers.set('X-Request-ID', String(xRequestID)); } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); + if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { + headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); } // authentication (sessionCookie) required - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + if (this.configuration.apiKeys) { + const key: string | undefined = this.configuration.apiKeys["sessionCookie"] || this.configuration.apiKeys["sessionCookie"]; + if (key) { + } + } + + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.get(`${this.configuration.basePath}/v1/search/bankSearch`, { params: queryParameters, + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, diff --git a/fintech-examples/fintech-ui/src/app/api/api/finTechOauth2Authentication.service.ts b/fintech-examples/fintech-ui/src/app/api/api/finTechOauth2Authentication.service.ts index 82e36f1f2a..79a9d29dcd 100644 --- a/fintech-examples/fintech-ui/src/app/api/api/finTechOauth2Authentication.service.ts +++ b/fintech-examples/fintech-ui/src/app/api/api/finTechOauth2Authentication.service.ts @@ -48,6 +48,42 @@ export class FinTechOauth2AuthenticationService { + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, + (value as Date).toISOString().substr(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + /** * Identifies the PSU in the Realm of the FinTechApi using his Gmail or other IDP provider account. * Use Oauth2 for Gmail users\' account to identify him @@ -56,10 +92,10 @@ export class FinTechOauth2AuthenticationService { * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public oauthLoginPOST(xRequestID: string, idpProvider: 'gmail', observe?: 'body', reportProgress?: boolean): Observable; - public oauthLoginPOST(xRequestID: string, idpProvider: 'gmail', observe?: 'response', reportProgress?: boolean): Observable>; - public oauthLoginPOST(xRequestID: string, idpProvider: 'gmail', observe?: 'events', reportProgress?: boolean): Observable>; - public oauthLoginPOST(xRequestID: string, idpProvider: 'gmail', observe: any = 'body', reportProgress: boolean = false ): Observable { + public oauthLoginPOST(xRequestID: string, idpProvider: 'gmail', observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined}): Observable; + public oauthLoginPOST(xRequestID: string, idpProvider: 'gmail', observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined}): Observable>; + public oauthLoginPOST(xRequestID: string, idpProvider: 'gmail', observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined}): Observable>; + public oauthLoginPOST(xRequestID: string, idpProvider: 'gmail', observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined}): Observable { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling oauthLoginPOST.'); } @@ -72,18 +108,27 @@ export class FinTechOauth2AuthenticationService { headers = headers.set('X-Request-ID', String(xRequestID)); } - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.post(`${this.configuration.basePath}/v1/oauth2/${encodeURIComponent(String(idpProvider))}/login`, null, { + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, diff --git a/fintech-examples/fintech-ui/src/app/api/api/fintechRetrieveAllSinglePayments.service.ts b/fintech-examples/fintech-ui/src/app/api/api/fintechRetrieveAllSinglePayments.service.ts index e47557badc..7ce7c7465e 100644 --- a/fintech-examples/fintech-ui/src/app/api/api/fintechRetrieveAllSinglePayments.service.ts +++ b/fintech-examples/fintech-ui/src/app/api/api/fintechRetrieveAllSinglePayments.service.ts @@ -17,8 +17,8 @@ import { HttpClient, HttpHeaders, HttpParams, import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; -import { ErrorResponse } from '../model/errorResponse'; -import { PaymentInitiationWithStatusResponse } from '../model/paymentInitiationWithStatusResponse'; +import { ErrorResponse } from '../model/models'; +import { PaymentInitiationWithStatusResponse } from '../model/models'; import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; import { Configuration } from '../configuration'; @@ -50,20 +50,56 @@ export class FintechRetrieveAllSinglePaymentsService { + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, + (value as Date).toISOString().substr(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + /** * Ask for all payments of this account * This method is used to get payment status. * @param bankId * @param accountId * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. + * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public retrieveAllSinglePayments(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, observe?: 'body', reportProgress?: boolean): Observable>; - public retrieveAllSinglePayments(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, observe?: 'response', reportProgress?: boolean): Observable>>; - public retrieveAllSinglePayments(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, observe?: 'events', reportProgress?: boolean): Observable>>; - public retrieveAllSinglePayments(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, observe: any = 'body', reportProgress: boolean = false ): Observable { + public retrieveAllSinglePayments(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public retrieveAllSinglePayments(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>>; + public retrieveAllSinglePayments(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>>; + public retrieveAllSinglePayments(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (bankId === null || bankId === undefined) { throw new Error('Required parameter bankId was null or undefined when calling retrieveAllSinglePayments.'); } @@ -73,30 +109,39 @@ export class FintechRetrieveAllSinglePaymentsService { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling retrieveAllSinglePayments.'); } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling retrieveAllSinglePayments.'); + if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { + throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling retrieveAllSinglePayments.'); } let headers = this.defaultHeaders; if (xRequestID !== undefined && xRequestID !== null) { headers = headers.set('X-Request-ID', String(xRequestID)); } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); + if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { + headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); } - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.get>(`${this.configuration.basePath}/v1/pis/banks/${encodeURIComponent(String(bankId))}/accounts/${encodeURIComponent(String(accountId))}/payments/single`, { + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, diff --git a/fintech-examples/fintech-ui/src/app/api/api/fintechRetrieveConsent.service.ts b/fintech-examples/fintech-ui/src/app/api/api/fintechRetrieveConsent.service.ts new file mode 100644 index 0000000000..1012a08dfe --- /dev/null +++ b/fintech-examples/fintech-ui/src/app/api/api/fintechRetrieveConsent.service.ts @@ -0,0 +1,137 @@ +/** + * Open Banking Gateway FinTech Example API + * This is a sample API that shows how to develop FinTech use cases that invoke banking APIs. #### User Agent and Cookies This Api assumes * that the PsuUserAgent (hosting the FinTechUI) is a modern web browser that stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). * that any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of a modern browser with respect to Cookies. #### SessionCookies and XSRF After a PSU is authenticated with the FinTech environment (either through the simple login interface defined here, or through an identity provider), the FinTechApi will establish a session with the FinTechUI. This is done by the mean of using a cookie called SessionCookie. This SessionCookie is protected by a corresponding xsrfToken. The response that sets a SessionCookie also carries a corresponding xsrfToken in the response header named \"X-XSRF-TOKEN\". It is the responsibility of the FinTechUI to : * parse and store this xsrfToken so that a refresh of a browser window can work. This shall be done using user agent capabilities. A web browser application might decide to store the xsrfToken in the browser localStorage, as the cookie we set are all considered persistent. * make sure that each subsequent request that is carrying the SessionCookie also carries the corresponding xsrfToken as header field (see the request path). * remove this xsrfToken from the localStorage when the corresponding SessionCookie is deleted by a server response (setting cookie value to null). The main difference between an xsrfToken and a SessionCookie is that the sessionCookie is automatically sent with each matching request. The xsrfToken must be explicitely read and sent by application. #### API- vs. UI-Redirection For simplicity, this Framework is designed to redirect to FinTechUI not to FinTechApi. #### Explicite vs. Implicite Redirection We define an \"Implicite redirection\" a case where a web browser react to 30X reponse and automatically redirects to the attached endpoint. We define an \"Explicite Redirection\" as a case where the UI-Application reacts to a 20X response, explicitely parses the attached __Location__ header an uses it to reload the new page in the browser window (or start the new UI-Application in case of native apps). This framework advocates for explicite redirection passing a __20X__ response to the FinTechUI toghether with the __Location__ parameter. Processing a response that initiates a redirect, the FinTechUI makes sure following happens, * that the exisitng __SessionCookie__ is deleted, as the user will not have a chance for an explicite logout, * that the corresponding xsrfToken is deleted from the local storage, * that a RedirectCookie set is stored (in case UI is not a web browser), so the user can be authenticated against it when sent back to the FinTechUI. The expiration of the RedirectCookie shall be set to the expected duration of the redirect, * that the corresponding xsrfToken is stored in the local storage (under the same cookie path as the RedirectCookie) #### Redirecting to the ConsentAuthorisationApi For a redirection to the ConsentAuthorisationApi, a generated AUTH-ID is added to the cookie path and used to distinguish authorization processes from each order. This information (AUTH-ID) must be contained in the back redirect url sent to the ConsentAuthorisationApi in the back channel, so that the FinTechUI can invoke the correct code2Token endpoint when activated. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + + +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; + + + +@Injectable({ + providedIn: 'root' +}) +export class FintechRetrieveConsentService { + + protected basePath = 'http://localhost'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string, @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, + (value as Date).toISOString().substr(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * ask for existing consent of user + * This method is disabled by default. It can be enabled by profile \"CONSENT_RETRIEVAL\" + * @param userid + * @param password + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public retrieveConsent(userid: string, password: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public retrieveConsent(userid: string, password: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public retrieveConsent(userid: string, password: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public retrieveConsent(userid: string, password: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { + if (userid === null || userid === undefined) { + throw new Error('Required parameter userid was null or undefined when calling retrieveConsent.'); + } + if (password === null || password === undefined) { + throw new Error('Required parameter password was null or undefined when calling retrieveConsent.'); + } + + let headers = this.defaultHeaders; + + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (httpHeaderAcceptSelected !== undefined) { + headers = headers.set('Accept', httpHeaderAcceptSelected); + } + + + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + + return this.httpClient.get(`${this.configuration.basePath}/v1/consent/${encodeURIComponent(String(userid))}/${encodeURIComponent(String(password))}`, + { + responseType: responseType, + withCredentials: this.configuration.withCredentials, + headers: headers, + observe: observe, + reportProgress: reportProgress + } + ); + } + +} diff --git a/fintech-examples/fintech-ui/src/app/api/api/fintechSinglePaymentInitiation.service.ts b/fintech-examples/fintech-ui/src/app/api/api/fintechSinglePaymentInitiation.service.ts index 438b568f99..523c8b9af5 100644 --- a/fintech-examples/fintech-ui/src/app/api/api/fintechSinglePaymentInitiation.service.ts +++ b/fintech-examples/fintech-ui/src/app/api/api/fintechSinglePaymentInitiation.service.ts @@ -1,9 +1,9 @@ /** * Open Banking Gateway FinTech Example API - * This is a sample API that shows how to develop FinTech use cases that invoke banking APIs. #### User Agent and Cookies This Api assumes * that the PsuUserAgent (hosting the FinTechUI) is a modern web browser that stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). * that any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of a modern browser with respect to Cookies. #### SessionCookies and XSRF After a PSU is authenticated with the FinTech environment (either through the simple login interface defined here, or through an identity provider), the FinTechApi will establish a session with the FinTechUI. This is done by the mean of using a cookie called SessionCookie. This SessionCookie is protected by a corresponding xsrfToken. The response that sets a SessionCookie also carries a corresponding xsrfToken in the response header named \"X-XSRF-TOKEN\". It is the responsibility of the FinTechUI to : * parse and store this xsrfToken so that a refresh of a browser window can work. This shall be done using user agent capabilities. A web browser application might decide to store the xsrfToken in the browser localStorage, as the cookie we set are all considered persistent. * make sure that each subsequent request that is carrying the SessionCookie also carries the corresponding xsrfToken as header field (see the request path). * remove this xsrfToken from the localStorage when the corresponding SessionCookie is deleted by a server response (setting cookie value to null). The main difference between an xsrfToken and a SessionCookie is that the sessionCookie is automatically sent with each matching request. The xsrfToken must be explicitely read and sent by application. #### API- vs. UI-Redirection For simplicity, this Framework is designed to redirect to FinTechUI not to FinTechApi. #### Explicite vs. Implicite Redirection We define an \"Implicite redirection\" a case where a web browser react to 30X reponse and automatically redirects to the attached endpoint. We define an \"Explicite Redirection\" as a case where the UI-Application reacts to a 20X response, explicitely parses the attached __Location__ header an uses it to reload the new page in the browser window (or start the new UI-Application in case of native apps). This framework advocates for explicite redirection passing a __20X__ response to the FinTechUI toghether with the __Location__ parameter. Processing a response that initiates a redirect, the FinTechUI makes sure following happens, * that the exisitng __SessionCookie__ is deleted, as the user will not have a chance for an explicite logout, * that the corresponding xsrfToken is deleted from the local storage, * that a RedirectCookie set is stored (in case UI is not a web browser), so the user can be authenticated against it when sent back to the FinTechUI. The expiration of the RedirectCookie shall be set to the expected duration of the redirect, * that the corresponding xsrfToken is stored in the local storage (under the same cookie path as the RedirectCookie) #### Redirecting to the ConsentAuthorisationApi For a redirection to the ConsentAuthorisationApi, a generated AUTH-ID is added to the cookie path and used to distinguish authorization processes from each order. This information (AUTH-ID) must be contained in the back redirect url sent to the ConsentAuthorisationApi in the back channel, so that the FinTechUI can invoke the correct code2Token endpoint when activated. + * This is a sample API that shows how to develop FinTech use cases that invoke banking APIs. #### User Agent and Cookies This Api assumes * that the PsuUserAgent (hosting the FinTechUI) is a modern web browser that stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). * that any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of a modern browser with respect to Cookies. #### SessionCookies and XSRF After a PSU is authenticated with the FinTech environment (either through the simple login interface defined here, or through an identity provider), the FinTechApi will establish a session with the FinTechUI. This is done by the mean of using a cookie called SessionCookie. This SessionCookie is protected by a corresponding xsrfToken. The response that sets a SessionCookie also carries a corresponding xsrfToken in the response header named \"X-XSRF-TOKEN\". It is the responsibility of the FinTechUI to : * parse and store this xsrfToken so that a refresh of a browser window can work. This shall be done using user agent capabilities. A web browser application might decide to store the xsrfToken in the browser localStorage, as the cookie we set are all considered persistent. * make sure that each subsequent request that is carrying the SessionCookie also carries the corresponding xsrfToken as header field (see the request path). * remove this xsrfToken from the localStorage when the corresponding SessionCookie is deleted by a server response (setting cookie value to null). The main difference between an xsrfToken and a SessionCookie is that the sessionCookie is automatically sent with each matching request. The xsrfToken must be explicitely read and sent by application. #### API- vs. UI-Redirection For simplicity, this Framework is designed to redirect to FinTechUI not to FinTechApi. #### Explicite vs. Implicite Redirection We define an \"Implicite redirection\" a case where a web browser react to 30X reponse and automatically redirects to the attached endpoint. We define an \"Explicite Redirection\" as a case where the UI-Application reacts to a 20X response, explicitely parses the attached __Location__ header an uses it to reload the new page in the browser window (or start the new UI-Application in case of native apps). This framework advocates for explicite redirection passing a __20X__ response to the FinTechUI toghether with the __Location__ parameter. Processing a response that initiates a redirect, the FinTechUI makes sure following happens, * that the exisitng __SessionCookie__ is deleted, as the user will not have a chance for an explicite logout, * that the corresponding xsrfToken is deleted from the local storage, * that a RedirectCookie set is stored (in case UI is not a web browser), so the user can be authenticated against it when sent back to the FinTechUI. The expiration of the RedirectCookie shall be set to the expected duration of the redirect, * that the corresponding xsrfToken is stored in the local storage (under the same cookie path as the RedirectCookie) #### Redirecting to the ConsentAuthorisationApi For a redirection to the ConsentAuthorisationApi, a generated AUTH-ID is added to the cookie path and used to distinguish authorization processes from each order. This information (AUTH-ID) must be contained in the back redirect url sent to the ConsentAuthorisationApi in the back channel, so that the FinTechUI can invoke the correct code2Token endpoint when activated. * * The version of the OpenAPI document: 1.0.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -17,8 +17,8 @@ import { HttpClient, HttpHeaders, HttpParams, import { CustomHttpParameterCodec } from '../encoder'; import { Observable } from 'rxjs'; -import { ErrorResponse } from '../model/errorResponse'; -import { SinglePaymentInitiationRequest } from '../model/singlePaymentInitiationRequest'; +import { ErrorResponse } from '../model/models'; +import { SinglePaymentInitiationRequest } from '../model/models'; import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; import { Configuration } from '../configuration'; @@ -50,24 +50,60 @@ export class FintechSinglePaymentInitiationService { + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, + (value as Date).toISOString().substr(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + /** * Single payment initiation request * This method is used to initiate a payment at the Fintech Server. - * @param bankId - * @param accountId - * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. - * @param X_XSRF_TOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. - * @param fintechRedirectURLOK - * @param fintechRedirectURLNOK + * @param bankId + * @param accountId + * @param xRequestID Unique ID that identifies this request through common workflow. Must be contained in HTTP Response as well. + * @param xXSRFTOKEN XSRF parameter used to validate a SessionCookie or RedirectCookie. + * @param fintechRedirectURLOK + * @param fintechRedirectURLNOK * @param singlePaymentInitiationRequest Single payment initiation request - * @param xPisPsuAuthenticationRequired If false, login form to OPBA will not be displayed as there might be nothing to share for payments, so that authentication is not necessary. If absent or true - login form for payments will be displayed. + * @param xPsuAuthenticationRequired If false, login form to OPBA will not be displayed as there might be nothing to share for payments, so that authentication is not necessary. If absent or true - login form for payments will be displayed. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public initiateSinglePayment(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, singlePaymentInitiationRequest: SinglePaymentInitiationRequest, xPisPsuAuthenticationRequired?: boolean, observe?: 'body', reportProgress?: boolean): Observable; - public initiateSinglePayment(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, singlePaymentInitiationRequest: SinglePaymentInitiationRequest, xPisPsuAuthenticationRequired?: boolean, observe?: 'response', reportProgress?: boolean): Observable>; - public initiateSinglePayment(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, singlePaymentInitiationRequest: SinglePaymentInitiationRequest, xPisPsuAuthenticationRequired?: boolean, observe?: 'events', reportProgress?: boolean): Observable>; - public initiateSinglePayment(bankId: string, accountId: string, xRequestID: string, X_XSRF_TOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, singlePaymentInitiationRequest: SinglePaymentInitiationRequest, xPisPsuAuthenticationRequired?: boolean, observe: any = 'body', reportProgress: boolean = false ): Observable { + public initiateSinglePayment(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, singlePaymentInitiationRequest: SinglePaymentInitiationRequest, xPsuAuthenticationRequired?: boolean, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable; + public initiateSinglePayment(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, singlePaymentInitiationRequest: SinglePaymentInitiationRequest, xPsuAuthenticationRequired?: boolean, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public initiateSinglePayment(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, singlePaymentInitiationRequest: SinglePaymentInitiationRequest, xPsuAuthenticationRequired?: boolean, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable>; + public initiateSinglePayment(bankId: string, accountId: string, xRequestID: string, xXSRFTOKEN: string, fintechRedirectURLOK: string, fintechRedirectURLNOK: string, singlePaymentInitiationRequest: SinglePaymentInitiationRequest, xPsuAuthenticationRequired?: boolean, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable { if (bankId === null || bankId === undefined) { throw new Error('Required parameter bankId was null or undefined when calling initiateSinglePayment.'); } @@ -77,8 +113,8 @@ export class FintechSinglePaymentInitiationService { if (xRequestID === null || xRequestID === undefined) { throw new Error('Required parameter xRequestID was null or undefined when calling initiateSinglePayment.'); } - if (X_XSRF_TOKEN === null || X_XSRF_TOKEN === undefined) { - throw new Error('Required parameter X_XSRF_TOKEN was null or undefined when calling initiateSinglePayment.'); + if (xXSRFTOKEN === null || xXSRFTOKEN === undefined) { + throw new Error('Required parameter xXSRFTOKEN was null or undefined when calling initiateSinglePayment.'); } if (fintechRedirectURLOK === null || fintechRedirectURLOK === undefined) { throw new Error('Required parameter fintechRedirectURLOK was null or undefined when calling initiateSinglePayment.'); @@ -94,11 +130,11 @@ export class FintechSinglePaymentInitiationService { if (xRequestID !== undefined && xRequestID !== null) { headers = headers.set('X-Request-ID', String(xRequestID)); } - if (X_XSRF_TOKEN !== undefined && X_XSRF_TOKEN !== null) { - headers = headers.set('X-XSRF-TOKEN', String(X_XSRF_TOKEN)); + if (xXSRFTOKEN !== undefined && xXSRFTOKEN !== null) { + headers = headers.set('X-XSRF-TOKEN', String(xXSRFTOKEN)); } - if (xPisPsuAuthenticationRequired !== undefined && xPisPsuAuthenticationRequired !== null) { - headers = headers.set('X-Pis-Psu-Authentication-Required', String(xPisPsuAuthenticationRequired)); + if (xPsuAuthenticationRequired !== undefined && xPsuAuthenticationRequired !== null) { + headers = headers.set('X-Psu-Authentication-Required', String(xPsuAuthenticationRequired)); } if (fintechRedirectURLOK !== undefined && fintechRedirectURLOK !== null) { headers = headers.set('Fintech-Redirect-URL-OK', String(fintechRedirectURLOK)); @@ -107,11 +143,14 @@ export class FintechSinglePaymentInitiationService { headers = headers.set('Fintech-Redirect-URL-NOK', String(fintechRedirectURLNOK)); } - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts); + let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (httpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } @@ -126,9 +165,15 @@ export class FintechSinglePaymentInitiationService { headers = headers.set('Content-Type', httpContentTypeSelected); } + let responseType: 'text' | 'json' = 'json'; + if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { + responseType = 'text'; + } + return this.httpClient.post(`${this.configuration.basePath}/v1/pis/banks/${encodeURIComponent(String(bankId))}/accounts/${encodeURIComponent(String(accountId))}/payments/single`, singlePaymentInitiationRequest, { + responseType: responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, diff --git a/fintech-examples/fintech-ui/src/app/api/model/accountBalance.ts b/fintech-examples/fintech-ui/src/app/api/model/accountBalance.ts index 989ef34643..c23463625e 100644 --- a/fintech-examples/fintech-ui/src/app/api/model/accountBalance.ts +++ b/fintech-examples/fintech-ui/src/app/api/model/accountBalance.ts @@ -16,7 +16,7 @@ export interface AccountBalance { balanceAmount?: Amount; balanceType?: string; iban?: string; - lastChangeDateTime?: Date; + lastChangeDateTime?: string; lastCommittedTransaction?: string; referenceDate?: string; } diff --git a/fintech-examples/fintech-ui/src/app/api/model/accountDetails.ts b/fintech-examples/fintech-ui/src/app/api/model/accountDetails.ts index 75356efdd0..26cb0ecd3c 100644 --- a/fintech-examples/fintech-ui/src/app/api/model/accountDetails.ts +++ b/fintech-examples/fintech-ui/src/app/api/model/accountDetails.ts @@ -76,7 +76,7 @@ export interface AccountDetails { * Specifications that might be provided by the ASPSP: - characteristics of the account - characteristics of the relevant card */ details?: string; - links?: LinksAccountDetails; + _links?: LinksAccountDetails; /** * Name of the legal account owner. If there is more than one owner, then e.g. two names might be noted here. */ diff --git a/fintech-examples/fintech-ui/src/app/api/model/accountReport.ts b/fintech-examples/fintech-ui/src/app/api/model/accountReport.ts index bdc35c2b25..6a2ab38ef4 100644 --- a/fintech-examples/fintech-ui/src/app/api/model/accountReport.ts +++ b/fintech-examples/fintech-ui/src/app/api/model/accountReport.ts @@ -1,9 +1,9 @@ /** * Open Banking Gateway FinTech Example API - * This is a sample API that shows how to develop FinTech use cases that invoke banking APIs. #### User Agent and Cookies This Api assumes * that the PsuUserAgent (hosting the FinTechUI) is a modern web browser that stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). * that any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of a modern browser with respect to Cookies. #### SessionCookies and XSRF After a PSU is authenticated with the FinTech environment (either through the simple login interface defined here, or through an identity provider), the FinTechApi will establish a session with the FinTechUI. This is done by the mean of using a cookie called SessionCookie. This SessionCookie is protected by a corresponding xsrfToken. The response that sets a SessionCookie also carries a corresponding xsrfToken in the response header named \"X-XSRF-TOKEN\". It is the responsibility of the FinTechUI to : * parse and store this xsrfToken so that a refresh of a browser window can work. This shall be done using user agent capabilities. A web browser application might decide to store the xsrfToken in the browser localStorage, as the cookie we set are all considered persistent. * make sure that each subsequent request that is carrying the SessionCookie also carries the corresponding xsrfToken as header field (see the request path). * remove this xsrfToken from the localStorage when the corresponding SessionCookie is deleted by a server response (setting cookie value to null). The main difference between an xsrfToken and a SessionCookie is that the sessionCookie is automatically sent with each matching request. The xsrfToken must be explicitely read and sent by application. #### API- vs. UI-Redirection For simplicity, this Framework is designed to redirect to FinTechUI not to FinTechApi. #### Explicite vs. Implicite Redirection We define an \"Implicite redirection\" a case where a web browser react to 30X reponse and automatically redirects to the attached endpoint. We define an \"Explicite Redirection\" as a case where the UI-Application reacts to a 20X response, explicitely parses the attached __Location__ header an uses it to reload the new page in the browser window (or start the new UI-Application in case of native apps). This framework advocates for explicite redirection passing a __20X__ response to the FinTechUI toghether with the __Location__ parameter. Processing a response that initiates a redirect, the FinTechUI makes sure following happens, * that the exisitng __SessionCookie__ is deleted, as the user will not have a chance for an explicite logout, * that the corresponding xsrfToken is deleted from the local storage, * that a RedirectCookie set is stored (in case UI is not a web browser), so the user can be authenticated against it when sent back to the FinTechUI. The expiration of the RedirectCookie shall be set to the expected duration of the redirect, * that the corresponding xsrfToken is stored in the local storage (under the same cookie path as the RedirectCookie) #### Redirecting to the ConsentAuthorisationApi For a redirection to the ConsentAuthorisationApi, a generated AUTH-ID is added to the cookie path and used to distinguish authorization processes from each order. This information (AUTH-ID) must be contained in the back redirect url sent to the ConsentAuthorisationApi in the back channel, so that the FinTechUI can invoke the correct code2Token endpoint when activated. + * This is a sample API that shows how to develop FinTech use cases that invoke banking APIs. #### User Agent and Cookies This Api assumes * that the PsuUserAgent (hosting the FinTechUI) is a modern web browser that stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). * that any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of a modern browser with respect to Cookies. #### SessionCookies and XSRF After a PSU is authenticated with the FinTech environment (either through the simple login interface defined here, or through an identity provider), the FinTechApi will establish a session with the FinTechUI. This is done by the mean of using a cookie called SessionCookie. This SessionCookie is protected by a corresponding xsrfToken. The response that sets a SessionCookie also carries a corresponding xsrfToken in the response header named \"X-XSRF-TOKEN\". It is the responsibility of the FinTechUI to : * parse and store this xsrfToken so that a refresh of a browser window can work. This shall be done using user agent capabilities. A web browser application might decide to store the xsrfToken in the browser localStorage, as the cookie we set are all considered persistent. * make sure that each subsequent request that is carrying the SessionCookie also carries the corresponding xsrfToken as header field (see the request path). * remove this xsrfToken from the localStorage when the corresponding SessionCookie is deleted by a server response (setting cookie value to null). The main difference between an xsrfToken and a SessionCookie is that the sessionCookie is automatically sent with each matching request. The xsrfToken must be explicitely read and sent by application. #### API- vs. UI-Redirection For simplicity, this Framework is designed to redirect to FinTechUI not to FinTechApi. #### Explicite vs. Implicite Redirection We define an \"Implicite redirection\" a case where a web browser react to 30X reponse and automatically redirects to the attached endpoint. We define an \"Explicite Redirection\" as a case where the UI-Application reacts to a 20X response, explicitely parses the attached __Location__ header an uses it to reload the new page in the browser window (or start the new UI-Application in case of native apps). This framework advocates for explicite redirection passing a __20X__ response to the FinTechUI toghether with the __Location__ parameter. Processing a response that initiates a redirect, the FinTechUI makes sure following happens, * that the exisitng __SessionCookie__ is deleted, as the user will not have a chance for an explicite logout, * that the corresponding xsrfToken is deleted from the local storage, * that a RedirectCookie set is stored (in case UI is not a web browser), so the user can be authenticated against it when sent back to the FinTechUI. The expiration of the RedirectCookie shall be set to the expected duration of the redirect, * that the corresponding xsrfToken is stored in the local storage (under the same cookie path as the RedirectCookie) #### Redirecting to the ConsentAuthorisationApi For a redirection to the ConsentAuthorisationApi, a generated AUTH-ID is added to the cookie path and used to distinguish authorization processes from each order. This information (AUTH-ID) must be contained in the back redirect url sent to the ConsentAuthorisationApi in the back channel, so that the FinTechUI can invoke the correct code2Token endpoint when activated. * * The version of the OpenAPI document: 1.0.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -13,9 +13,9 @@ import { TransactionDetails } from './transactionDetails'; /** - * JSON based account report. This account report contains transactions resulting from the query parameters. \'booked\' shall be contained if bookingStatus parameter is set to \"booked\" or \"both\". \'pending\' is not contained if the bookingStatus parameter is set to \"booked\". + * JSON based account report. This account report contains transactions resulting from the query parameters. \'booked\' shall be contained if bookingStatus parameter is set to \"booked\" or \"both\". \'pending\' is not contained if the bookingStatus parameter is set to \"booked\". */ -export interface AccountReport { +export interface AccountReport { /** * Array of transaction details. */ @@ -24,5 +24,9 @@ export interface AccountReport { * Array of transaction details. */ pending?: Array; + /** + * Raw bank response as String + */ + rawTransactions?: string; } diff --git a/fintech-examples/fintech-ui/src/app/api/model/aisAccountAccessInfo.ts b/fintech-examples/fintech-ui/src/app/api/model/aisAccountAccessInfo.ts new file mode 100644 index 0000000000..72cd184c6d --- /dev/null +++ b/fintech-examples/fintech-ui/src/app/api/model/aisAccountAccessInfo.ts @@ -0,0 +1,53 @@ +/** + * Open Banking Gateway FinTech Example API + * This is a sample API that shows how to develop FinTech use cases that invoke banking APIs. #### User Agent and Cookies This Api assumes * that the PsuUserAgent (hosting the FinTechUI) is a modern web browser that stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). * that any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of a modern browser with respect to Cookies. #### SessionCookies and XSRF After a PSU is authenticated with the FinTech environment (either through the simple login interface defined here, or through an identity provider), the FinTechApi will establish a session with the FinTechUI. This is done by the mean of using a cookie called SessionCookie. This SessionCookie is protected by a corresponding xsrfToken. The response that sets a SessionCookie also carries a corresponding xsrfToken in the response header named \"X-XSRF-TOKEN\". It is the responsibility of the FinTechUI to : * parse and store this xsrfToken so that a refresh of a browser window can work. This shall be done using user agent capabilities. A web browser application might decide to store the xsrfToken in the browser localStorage, as the cookie we set are all considered persistent. * make sure that each subsequent request that is carrying the SessionCookie also carries the corresponding xsrfToken as header field (see the request path). * remove this xsrfToken from the localStorage when the corresponding SessionCookie is deleted by a server response (setting cookie value to null). The main difference between an xsrfToken and a SessionCookie is that the sessionCookie is automatically sent with each matching request. The xsrfToken must be explicitely read and sent by application. #### API- vs. UI-Redirection For simplicity, this Framework is designed to redirect to FinTechUI not to FinTechApi. #### Explicite vs. Implicite Redirection We define an \"Implicite redirection\" a case where a web browser react to 30X reponse and automatically redirects to the attached endpoint. We define an \"Explicite Redirection\" as a case where the UI-Application reacts to a 20X response, explicitely parses the attached __Location__ header an uses it to reload the new page in the browser window (or start the new UI-Application in case of native apps). This framework advocates for explicite redirection passing a __20X__ response to the FinTechUI toghether with the __Location__ parameter. Processing a response that initiates a redirect, the FinTechUI makes sure following happens, * that the exisitng __SessionCookie__ is deleted, as the user will not have a chance for an explicite logout, * that the corresponding xsrfToken is deleted from the local storage, * that a RedirectCookie set is stored (in case UI is not a web browser), so the user can be authenticated against it when sent back to the FinTechUI. The expiration of the RedirectCookie shall be set to the expected duration of the redirect, * that the corresponding xsrfToken is stored in the local storage (under the same cookie path as the RedirectCookie) #### Redirecting to the ConsentAuthorisationApi For a redirection to the ConsentAuthorisationApi, a generated AUTH-ID is added to the cookie path and used to distinguish authorization processes from each order. This information (AUTH-ID) must be contained in the back redirect url sent to the ConsentAuthorisationApi in the back channel, so that the FinTechUI can invoke the correct code2Token endpoint when activated. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { AccountReference } from './accountReference'; + + +/** + * Ais account access information + */ +export interface AisAccountAccessInfo { + /** + * Access to accounts + */ + accounts?: Array; + /** + * Consent on all accounts, balances and transactions of psu + */ + allPsd2?: AisAccountAccessInfo.AllPsd2Enum; + /** + * Consent on all available accounts of psu + */ + availableAccounts?: AisAccountAccessInfo.AvailableAccountsEnum; + /** + * Access to balances + */ + balances?: Array; + /** + * Access to transactions + */ + transactions?: Array; +} +export namespace AisAccountAccessInfo { + export type AllPsd2Enum = 'ALL_ACCOUNTS' | 'ALL_ACCOUNTS_WITH_BALANCES'; + export const AllPsd2Enum = { + ACCOUNTS: 'ALL_ACCOUNTS' as AllPsd2Enum, + ACCOUNTSWITHBALANCES: 'ALL_ACCOUNTS_WITH_BALANCES' as AllPsd2Enum + }; + export type AvailableAccountsEnum = 'ALL_ACCOUNTS' | 'ALL_ACCOUNTS_WITH_BALANCES'; + export const AvailableAccountsEnum = { + ACCOUNTS: 'ALL_ACCOUNTS' as AvailableAccountsEnum, + ACCOUNTSWITHBALANCES: 'ALL_ACCOUNTS_WITH_BALANCES' as AvailableAccountsEnum + }; +} + + diff --git a/fintech-examples/fintech-ui/src/app/api/model/aisConsentRequest.ts b/fintech-examples/fintech-ui/src/app/api/model/aisConsentRequest.ts new file mode 100644 index 0000000000..8f0fb7093d --- /dev/null +++ b/fintech-examples/fintech-ui/src/app/api/model/aisConsentRequest.ts @@ -0,0 +1,37 @@ +/** + * Open Banking Gateway FinTech Example API + * This is a sample API that shows how to develop FinTech use cases that invoke banking APIs. #### User Agent and Cookies This Api assumes * that the PsuUserAgent (hosting the FinTechUI) is a modern web browser that stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). * that any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of a modern browser with respect to Cookies. #### SessionCookies and XSRF After a PSU is authenticated with the FinTech environment (either through the simple login interface defined here, or through an identity provider), the FinTechApi will establish a session with the FinTechUI. This is done by the mean of using a cookie called SessionCookie. This SessionCookie is protected by a corresponding xsrfToken. The response that sets a SessionCookie also carries a corresponding xsrfToken in the response header named \"X-XSRF-TOKEN\". It is the responsibility of the FinTechUI to : * parse and store this xsrfToken so that a refresh of a browser window can work. This shall be done using user agent capabilities. A web browser application might decide to store the xsrfToken in the browser localStorage, as the cookie we set are all considered persistent. * make sure that each subsequent request that is carrying the SessionCookie also carries the corresponding xsrfToken as header field (see the request path). * remove this xsrfToken from the localStorage when the corresponding SessionCookie is deleted by a server response (setting cookie value to null). The main difference between an xsrfToken and a SessionCookie is that the sessionCookie is automatically sent with each matching request. The xsrfToken must be explicitely read and sent by application. #### API- vs. UI-Redirection For simplicity, this Framework is designed to redirect to FinTechUI not to FinTechApi. #### Explicite vs. Implicite Redirection We define an \"Implicite redirection\" a case where a web browser react to 30X reponse and automatically redirects to the attached endpoint. We define an \"Explicite Redirection\" as a case where the UI-Application reacts to a 20X response, explicitely parses the attached __Location__ header an uses it to reload the new page in the browser window (or start the new UI-Application in case of native apps). This framework advocates for explicite redirection passing a __20X__ response to the FinTechUI toghether with the __Location__ parameter. Processing a response that initiates a redirect, the FinTechUI makes sure following happens, * that the exisitng __SessionCookie__ is deleted, as the user will not have a chance for an explicite logout, * that the corresponding xsrfToken is deleted from the local storage, * that a RedirectCookie set is stored (in case UI is not a web browser), so the user can be authenticated against it when sent back to the FinTechUI. The expiration of the RedirectCookie shall be set to the expected duration of the redirect, * that the corresponding xsrfToken is stored in the local storage (under the same cookie path as the RedirectCookie) #### Redirecting to the ConsentAuthorisationApi For a redirection to the ConsentAuthorisationApi, a generated AUTH-ID is added to the cookie path and used to distinguish authorization processes from each order. This information (AUTH-ID) must be contained in the back redirect url sent to the ConsentAuthorisationApi in the back channel, so that the FinTechUI can invoke the correct code2Token endpoint when activated. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { AisAccountAccessInfo } from './aisAccountAccessInfo'; + + +/** + * Ais consent request + */ +export interface AisConsentRequest { + access: AisAccountAccessInfo; + /** + * Maximum frequency for an access per day. For a once-off access, this attribute is set to 1 + */ + frequencyPerDay: number; + /** + * \'true\', if the consent is for recurring access to the account data , \'false\', if the consent is for one access to the account data + */ + recurringIndicator: boolean; + /** + * Consent`s expiration date. The content is the local ASPSP date in ISODate Format + */ + validUntil: string; + /** + * \'true\', if the consent is i.e. account list and then payment \'false\', if the consent is for one access to the account data + */ + combinedServiceIndicator?: boolean; +} + diff --git a/fintech-examples/fintech-ui/src/app/api/model/analyticsReportDetails.ts b/fintech-examples/fintech-ui/src/app/api/model/analyticsReportDetails.ts new file mode 100644 index 0000000000..7538548862 --- /dev/null +++ b/fintech-examples/fintech-ui/src/app/api/model/analyticsReportDetails.ts @@ -0,0 +1,71 @@ +/** + * Open Banking Gateway FinTech Example API + * This is a sample API that shows how to develop FinTech use cases that invoke banking APIs. #### User Agent and Cookies This Api assumes * that the PsuUserAgent (hosting the FinTechUI) is a modern web browser that stores httpOnly cookies sent with the redirect under the given domain and path as defined by [RFC 6265](https://tools.ietf.org/html/rfc6265). * that any other PsuUserAgent like a native mobile or a desktop application can simulate this same behavior of a modern browser with respect to Cookies. #### SessionCookies and XSRF After a PSU is authenticated with the FinTech environment (either through the simple login interface defined here, or through an identity provider), the FinTechApi will establish a session with the FinTechUI. This is done by the mean of using a cookie called SessionCookie. This SessionCookie is protected by a corresponding xsrfToken. The response that sets a SessionCookie also carries a corresponding xsrfToken in the response header named \"X-XSRF-TOKEN\". It is the responsibility of the FinTechUI to : * parse and store this xsrfToken so that a refresh of a browser window can work. This shall be done using user agent capabilities. A web browser application might decide to store the xsrfToken in the browser localStorage, as the cookie we set are all considered persistent. * make sure that each subsequent request that is carrying the SessionCookie also carries the corresponding xsrfToken as header field (see the request path). * remove this xsrfToken from the localStorage when the corresponding SessionCookie is deleted by a server response (setting cookie value to null). The main difference between an xsrfToken and a SessionCookie is that the sessionCookie is automatically sent with each matching request. The xsrfToken must be explicitely read and sent by application. #### API- vs. UI-Redirection For simplicity, this Framework is designed to redirect to FinTechUI not to FinTechApi. #### Explicite vs. Implicite Redirection We define an \"Implicite redirection\" a case where a web browser react to 30X reponse and automatically redirects to the attached endpoint. We define an \"Explicite Redirection\" as a case where the UI-Application reacts to a 20X response, explicitely parses the attached __Location__ header an uses it to reload the new page in the browser window (or start the new UI-Application in case of native apps). This framework advocates for explicite redirection passing a __20X__ response to the FinTechUI toghether with the __Location__ parameter. Processing a response that initiates a redirect, the FinTechUI makes sure following happens, * that the exisitng __SessionCookie__ is deleted, as the user will not have a chance for an explicite logout, * that the corresponding xsrfToken is deleted from the local storage, * that a RedirectCookie set is stored (in case UI is not a web browser), so the user can be authenticated against it when sent back to the FinTechUI. The expiration of the RedirectCookie shall be set to the expected duration of the redirect, * that the corresponding xsrfToken is stored in the local storage (under the same cookie path as the RedirectCookie) #### Redirecting to the ConsentAuthorisationApi For a redirection to the ConsentAuthorisationApi, a generated AUTH-ID is added to the cookie path and used to distinguish authorization processes from each order. This information (AUTH-ID) must be contained in the back redirect url sent to the ConsentAuthorisationApi in the back channel, so that the FinTechUI can invoke the correct code2Token endpoint when activated. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +/** + * JSON based analytics report. This account report contains transaction categorization result. + */ +export interface AnalyticsReportDetails { + /** + * The id of transaction this analytics result refers to. + */ + transactionId?: string; + /** + * Main category of the booking. + */ + mainCategory?: string; + /** + * Sub category of the booking. + */ + subCategory?: string; + /** + * Specification of the booking. + */ + specification?: string; + /** + * Related account. + */ + otherAccount?: string; + /** + * Logo. + */ + logo?: string; + /** + * Homepage. + */ + homepage?: string; + /** + * Hotline. + */ + hotline?: string; + /** + * Email. + */ + email?: string; + /** + * Custom information about analyzed transaction. + */ + custom?: { [key: string]: string; }; + /** + * Rules that were used to analyze. + */ + usedRules?: Array; + /** + * Classification next booking date. + */ + nextBookingDate?: string; + /** + * Classification cycle result. + */ + cycle?: string; +} + diff --git a/fintech-examples/fintech-ui/src/app/api/model/bankDescriptor.ts b/fintech-examples/fintech-ui/src/app/api/model/bankDescriptor.ts index d4d4179241..e9c2ae4498 100644 --- a/fintech-examples/fintech-ui/src/app/api/model/bankDescriptor.ts +++ b/fintech-examples/fintech-ui/src/app/api/model/bankDescriptor.ts @@ -9,6 +9,7 @@ * https://openapi-generator.tech * Do not edit the class manually. */ +import { BankProfile } from './bankProfile'; export interface BankDescriptor { @@ -16,5 +17,6 @@ export interface BankDescriptor { bic?: string; bankCode?: string; uuid?: string; + profiles?: Array; } diff --git a/fintech-examples/fintech-ui/src/app/api/model/bankProfile.ts b/fintech-examples/fintech-ui/src/app/api/model/bankProfile.ts index 24d9ffe6a0..37cc2bf72e 100644 --- a/fintech-examples/fintech-ui/src/app/api/model/bankProfile.ts +++ b/fintech-examples/fintech-ui/src/app/api/model/bankProfile.ts @@ -14,7 +14,13 @@ export interface BankProfile { bankId?: string; bankName?: string; + name?: string; bic?: string; + uuid?: string; services?: Array; + externalId?: string; + externalInterfaces?: string; + protocolType?: string; + isSandbox?: boolean; } diff --git a/fintech-examples/fintech-ui/src/app/api/model/models.ts b/fintech-examples/fintech-ui/src/app/api/model/models.ts index b372391f79..32c3c686e3 100644 --- a/fintech-examples/fintech-ui/src/app/api/model/models.ts +++ b/fintech-examples/fintech-ui/src/app/api/model/models.ts @@ -6,6 +6,7 @@ export * from './accountReport'; export * from './accountStatus'; export * from './address'; export * from './amount'; +export * from './analyticsReportDetails'; export * from './bankDescriptor'; export * from './bankProfile'; export * from './errorResponse'; diff --git a/fintech-examples/fintech-ui/src/app/api/model/transactionsResponse.ts b/fintech-examples/fintech-ui/src/app/api/model/transactionsResponse.ts index b3a92672cb..7601d965a7 100644 --- a/fintech-examples/fintech-ui/src/app/api/model/transactionsResponse.ts +++ b/fintech-examples/fintech-ui/src/app/api/model/transactionsResponse.ts @@ -11,6 +11,7 @@ */ import { AccountReference } from './accountReference'; import { AccountReport } from './accountReport'; +import { AnalyticsReportDetails } from './analyticsReportDetails'; /** @@ -19,5 +20,9 @@ import { AccountReport } from './accountReport'; export interface TransactionsResponse { account?: AccountReference; transactions?: AccountReport; + /** + * Array of transaction details. + */ + analytics?: Array; } diff --git a/fintech-examples/fintech-ui/src/app/api/model/userProfile.ts b/fintech-examples/fintech-ui/src/app/api/model/userProfile.ts index b4b4c347d0..335c108303 100644 --- a/fintech-examples/fintech-ui/src/app/api/model/userProfile.ts +++ b/fintech-examples/fintech-ui/src/app/api/model/userProfile.ts @@ -13,6 +13,6 @@ export interface UserProfile { name?: string; - lastLogin?: Date; + lastLogin?: string; } diff --git a/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.html b/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.html index 444f78e404..609d677111 100644 --- a/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.html +++ b/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.html @@ -5,7 +5,7 @@

Search your bank

diff --git a/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.spec.ts b/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.spec.ts index 9eecac9a0f..2f423acd5b 100644 --- a/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.spec.ts +++ b/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.spec.ts @@ -3,9 +3,8 @@ import { ReactiveFormsModule } from '@angular/forms'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { BankSearchComponent } from './bank-search.component'; +import { BankSearchComponent, BankSearchInfo } from './bank-search.component'; import { SearchComponent } from '../common/search/search.component'; -import { BankDescriptor } from '../api'; import { BankSearchService } from './services/bank-search.service'; import { StorageService } from '../services/storage.service'; import { Router } from '@angular/router'; @@ -48,17 +47,12 @@ describe('BankSearchComponent', () => { }); it('should call onBankSelect', () => { - const mockBank: BankDescriptor = { - bankName: 'adorsys', - bic: 'DE230334244232322323', - bankCode: '12345', - uuid: 'xxxxxxxxxx', - }; + const mockBank: BankSearchInfo = new BankSearchInfo('bank1', '123'); const routerSpy = spyOn(router, 'navigate'); spyOn(component, 'onBankSelect').withArgs(mockBank).and.callThrough(); component.onBankSelect(mockBank); expect(routerSpy).toHaveBeenCalledWith([RoutingPath.BANK, mockBank.uuid]); - expect(component.selectedBank).toEqual(mockBank.uuid); + expect(component.selectedBankProfile).toEqual(mockBank.uuid); }); }); diff --git a/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.ts b/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.ts index 99306e6317..51c657b53e 100644 --- a/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.ts +++ b/fintech-examples/fintech-ui/src/app/bank-search/bank-search.component.ts @@ -1,10 +1,9 @@ -import { Component } from '@angular/core'; -import { BankSearchService } from './services/bank-search.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { BankDescriptor } from '../api'; -import { StorageService } from '../services/storage.service'; -import { TimerService } from '../services/timer.service'; -import { RoutingPath } from '../models/routing-path.model'; +import {Component} from '@angular/core'; +import {BankSearchService} from './services/bank-search.service'; +import {ActivatedRoute, Router} from '@angular/router'; +import {StorageService} from '../services/storage.service'; +import {TimerService} from '../services/timer.service'; +import {RoutingPath} from '../models/routing-path.model'; @Component({ selector: 'app-bank-search', @@ -12,8 +11,8 @@ import { RoutingPath } from '../models/routing-path.model'; styleUrls: ['./bank-search.component.scss'] }) export class BankSearchComponent { - searchedBanks: BankDescriptor[] = []; - selectedBank: string; + searchedBanks: BankSearchInfo[] = []; + selectedBankProfile: string; constructor( private bankSearchService: BankSearchService, @@ -28,21 +27,38 @@ export class BankSearchComponent { onSearch(keyword: string): void { if (keyword && keyword.trim()) { this.bankSearchService.searchBanks(keyword).subscribe((bankDescriptor) => { - this.searchedBanks = bankDescriptor.bankDescriptor; + this.searchedBanks = []; + for (const descriptor of bankDescriptor.bankDescriptor) { + if (!descriptor.profiles) { + continue + } + + this.searchedBanks.push(...descriptor.profiles.map(it => new BankSearchInfo(`[${it.protocolType}${null === it.name ? '' : ',' + it.name}] ${it.bankName}`, it.uuid))) + } }); } else { this.bankUnselect(); } } - onBankSelect(bank: BankDescriptor): void { - this.selectedBank = bank.uuid; - this.storageService.setBankName(bank.bankName); - this.router.navigate([RoutingPath.BANK, bank.uuid]); + onBankSelect(profile: BankSearchInfo): void { + this.selectedBankProfile = profile.uuid; + this.storageService.setBankName(profile.name); + this.router.navigate([RoutingPath.BANK, profile.uuid]); } private bankUnselect(): void { this.searchedBanks = []; - this.selectedBank = null; + this.selectedBankProfile = null; + } +} + +export class BankSearchInfo { + name: string + uuid: string + + constructor(name: string, profileId: string) { + this.name = name; + this.uuid = profileId; } } diff --git a/fintech-examples/fintech-ui/src/app/bank/list-accounts/list-accounts.component.ts b/fintech-examples/fintech-ui/src/app/bank/list-accounts/list-accounts.component.ts index ed26d9e246..08e88337b8 100644 --- a/fintech-examples/fintech-ui/src/app/bank/list-accounts/list-accounts.component.ts +++ b/fintech-examples/fintech-ui/src/app/bank/list-accounts/list-accounts.component.ts @@ -35,35 +35,45 @@ export class ListAccountsComponent implements OnInit { const settings = this.storageService.getSettings(); const online = !this.storageService.isAfterRedirect() && !settings.cacheLoa; this.storageService.setAfterRedirect(false); - this.aisService.getAccounts(this.bankId, settings.loa, settings.withBalance, online).subscribe((response) => { - switch (response.status) { - case 202: - this.storageService.setRedirect( - response.headers.get(HeaderConfig.HEADER_FIELD_REDIRECT_CODE), - response.headers.get(HeaderConfig.HEADER_FIELD_AUTH_ID), - response.headers.get(HeaderConfig.HEADER_FIELD_X_XSRF_TOKEN), - parseInt(response.headers.get(HeaderConfig.HEADER_FIELD_REDIRECT_X_MAX_AGE), 0), - RedirectType.AIS - ); - const r = new RedirectStruct(); - r.redirectUrl = encodeURIComponent(response.headers.get(HeaderConfig.HEADER_FIELD_LOCATION)); - r.redirectCode = response.headers.get(HeaderConfig.HEADER_FIELD_REDIRECT_CODE); - r.bankId = this.bankId; - r.bankName = this.storageService.getBankName(); - this.router.navigate(['redirect', JSON.stringify(r)], { relativeTo: this.route }); - break; - case 200: - // this is added to register url where to forward - // if LoT is cancelled after redirect page is displayed - // to be removed when issue https://github.com/adorsys/open-banking-gateway/issues/848 is resolved - // or Fintech UI refactored - this.accounts = response.body.accounts; - const loa = []; - for (const accountDetail of this.accounts) { - loa.push(new AccountStruct(accountDetail.resourceId, accountDetail.iban, accountDetail.name)); - } - this.storageService.setLoa(this.bankId, loa); - } - }); + + this.aisService + .getAccounts( + this.bankId, + settings.loa, + JSON.stringify(settings.consent), + settings.withBalance, + online, + settings.consentRequiresAuthentication + ) + .subscribe((response) => { + switch (response.status) { + case 202: + this.storageService.setRedirect( + response.headers.get(HeaderConfig.HEADER_FIELD_REDIRECT_CODE), + response.headers.get(HeaderConfig.HEADER_FIELD_AUTH_ID), + response.headers.get(HeaderConfig.HEADER_FIELD_X_XSRF_TOKEN), + parseInt(response.headers.get(HeaderConfig.HEADER_FIELD_REDIRECT_X_MAX_AGE), 0), + RedirectType.AIS + ); + const r = new RedirectStruct(); + r.redirectUrl = encodeURIComponent(response.headers.get(HeaderConfig.HEADER_FIELD_LOCATION)); + r.redirectCode = response.headers.get(HeaderConfig.HEADER_FIELD_REDIRECT_CODE); + r.bankId = this.bankId; + r.bankName = this.storageService.getBankName(); + this.router.navigate(['redirect', JSON.stringify(r)], { relativeTo: this.route }); + break; + case 200: + // this is added to register url where to forward + // if LoT is cancelled after redirect page is displayed + // to be removed when issue https://github.com/adorsys/open-banking-gateway/issues/848 is resolved + // or Fintech UI refactored + this.accounts = response.body.accounts; + const loa = []; + for (const accountDetail of this.accounts) { + loa.push(new AccountStruct(accountDetail.resourceId, accountDetail.iban, accountDetail.name)); + } + this.storageService.setLoa(this.bankId, loa); + } + }); } } diff --git a/fintech-examples/fintech-ui/src/app/bank/list-accounts/list.accounts.component.spec.ts b/fintech-examples/fintech-ui/src/app/bank/list-accounts/list.accounts.component.spec.ts index 8f20ee902d..e0401a07a8 100644 --- a/fintech-examples/fintech-ui/src/app/bank/list-accounts/list.accounts.component.spec.ts +++ b/fintech-examples/fintech-ui/src/app/bank/list-accounts/list.accounts.component.spec.ts @@ -55,10 +55,10 @@ describe('ListAccountsComponent', () => { const mockAccounts: HttpResponse = {} as HttpResponse; spyOn(aisService, 'getAccounts') - .withArgs(bankId, loaRetrievalInformation, false, true) + .withArgs(bankId, loaRetrievalInformation, '', false, true, true) .and.returnValue(of(mockAccounts)); expect(component.bankId).toEqual(bankId); - aisService.getAccounts(bankId, loaRetrievalInformation, false, true).subscribe((res) => { + aisService.getAccounts(bankId, loaRetrievalInformation, '', false, true, true).subscribe((res) => { expect(res).toEqual(mockAccounts); }); }); diff --git a/fintech-examples/fintech-ui/src/app/bank/list-transactions/list-transactions.component.html b/fintech-examples/fintech-ui/src/app/bank/list-transactions/list-transactions.component.html index 83586b7ca7..2548d46810 100644 --- a/fintech-examples/fintech-ui/src/app/bank/list-transactions/list-transactions.component.html +++ b/fintech-examples/fintech-ui/src/app/bank/list-transactions/list-transactions.component.html @@ -13,4 +13,6 @@ [title]="'Booked'" [isBookedTransaction]="transactions?.booked?.length > 0" > + +
{{ transactions?.rawTransactions }}
diff --git a/fintech-examples/fintech-ui/src/app/bank/list-transactions/list-transactions.component.ts b/fintech-examples/fintech-ui/src/app/bank/list-transactions/list-transactions.component.ts index c985f20d7e..8590a5a3c3 100644 --- a/fintech-examples/fintech-ui/src/app/bank/list-transactions/list-transactions.component.ts +++ b/fintech-examples/fintech-ui/src/app/bank/list-transactions/list-transactions.component.ts @@ -36,8 +36,16 @@ export class ListTransactionsComponent implements OnInit { private loadTransactions(): void { const settings = this.storageService.getSettings(); const online = !this.storageService.isAfterRedirect() && !settings.cacheLot; + this.aisService - .getTransactions(this.bankId, this.accountId, settings.lot, online) + .getTransactions( + this.bankId, + this.accountId, + settings.lot, + JSON.stringify(settings.consent), + online, + settings.consentRequiresAuthentication + ) .subscribe((response) => { switch (response.status) { case 202: diff --git a/fintech-examples/fintech-ui/src/app/bank/list-transactions/list.transactions.component.spec.ts b/fintech-examples/fintech-ui/src/app/bank/list-transactions/list.transactions.component.spec.ts index 0e41d48d02..92364b5f24 100644 --- a/fintech-examples/fintech-ui/src/app/bank/list-transactions/list.transactions.component.spec.ts +++ b/fintech-examples/fintech-ui/src/app/bank/list-transactions/list.transactions.component.spec.ts @@ -62,10 +62,10 @@ describe('ListTransactionsComponent', () => { const loTRetrievalInformation: LoTRetrievalInformation = LoTRetrievalInformation.FROM_TPP_WITH_AVAILABLE_CONSENT; spyOn(aisService, 'getTransactions') - .withArgs(bankId, accountId, loTRetrievalInformation, true) + .withArgs(bankId, accountId, loTRetrievalInformation, '', true, true) .and.returnValue(of(mockTransactions)); expect(component.bankId).toEqual(bankId); - aisService.getTransactions(bankId, accountId, loTRetrievalInformation, true).subscribe((res) => { + aisService.getTransactions(bankId, accountId, loTRetrievalInformation, '', true, true).subscribe((res) => { expect(res).toEqual(mockTransactions); }); }); diff --git a/fintech-examples/fintech-ui/src/app/bank/payment/payment-account-payments/payment-account-payments.component.spec.ts b/fintech-examples/fintech-ui/src/app/bank/payment/payment-account-payments/payment-account-payments.component.spec.ts index f880bee3d0..bd17b4b9f8 100644 --- a/fintech-examples/fintech-ui/src/app/bank/payment/payment-account-payments/payment-account-payments.component.spec.ts +++ b/fintech-examples/fintech-ui/src/app/bank/payment/payment-account-payments/payment-account-payments.component.spec.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { RouterTestingModule } from '@angular/router/testing'; -import {ReactiveFormsModule} from "@angular/forms"; +import { ReactiveFormsModule } from '@angular/forms'; import { PaymentAccountPaymentsComponent } from './payment-account-payments.component'; diff --git a/fintech-examples/fintech-ui/src/app/bank/payment/payment-accounts/payment-accounts.component.ts b/fintech-examples/fintech-ui/src/app/bank/payment/payment-accounts/payment-accounts.component.ts index 14eb15a55c..abb66556ec 100644 --- a/fintech-examples/fintech-ui/src/app/bank/payment/payment-accounts/payment-accounts.component.ts +++ b/fintech-examples/fintech-ui/src/app/bank/payment/payment-accounts/payment-accounts.component.ts @@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { StorageService } from '../../../services/storage.service'; import { Consts } from '../../../models/consts'; -import {ValidatorService} from "angular-iban"; -import {FormBuilder, FormGroup, Validators} from "@angular/forms"; +import { ValidatorService } from 'angular-iban'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; @Component({ selector: 'app-list-accounts-for-payment', @@ -17,8 +17,12 @@ export class PaymentAccountsComponent implements OnInit { accounts = []; ibanForm: FormGroup; - constructor(private storageService: StorageService, private formBuilder: FormBuilder, - private router: Router, private route: ActivatedRoute) {} + constructor( + private storageService: StorageService, + private formBuilder: FormBuilder, + private router: Router, + private route: ActivatedRoute + ) {} ngOnInit() { this.bankId = this.route.snapshot.params[Consts.BANK_ID_NAME]; @@ -36,8 +40,8 @@ export class PaymentAccountsComponent implements OnInit { } initiateSinglePayment() { - const iban = this.ibanForm.get('iban').value.replace(/\s/g, ""); - this.router.navigate(['../account/initiate'], {relativeTo: this.route, queryParams: {iban}}); + const iban = this.ibanForm.get('iban').value.replace(/\s/g, ''); + this.router.navigate(['../account/initiate'], { relativeTo: this.route, queryParams: { iban } }); } get iban() { diff --git a/fintech-examples/fintech-ui/src/app/bank/payment/payment-initiate/initiate.component.ts b/fintech-examples/fintech-ui/src/app/bank/payment/payment-initiate/initiate.component.ts index 967201c170..ee0f197faa 100644 --- a/fintech-examples/fintech-ui/src/app/bank/payment/payment-initiate/initiate.component.ts +++ b/fintech-examples/fintech-ui/src/app/bank/payment/payment-initiate/initiate.component.ts @@ -1,21 +1,19 @@ -import {Component, OnInit} from '@angular/core'; -import {FormBuilder, FormGroup, Validators} from '@angular/forms'; -import {HttpResponse} from '@angular/common/http'; -import {Location} from "@angular/common"; -import {ActivatedRoute, Router} from '@angular/router'; -import {ValidatorService} from 'angular-iban'; -import {FintechSinglePaymentInitiationService, SinglePaymentInitiationRequest} from '../../../api'; -import {Consts, HeaderConfig} from '../../../models/consts'; -import {RedirectStruct, RedirectType} from '../../redirect-page/redirect-struct'; -import {StorageService} from '../../../services/storage.service'; -import {ConfirmData} from '../payment-confirm/confirm.data'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { HttpResponse } from '@angular/common/http'; +import { Location } from '@angular/common'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ValidatorService } from 'angular-iban'; +import { FintechSinglePaymentInitiationService, SinglePaymentInitiationRequest } from '../../../api'; +import { Consts, HeaderConfig } from '../../../models/consts'; +import { RedirectStruct, RedirectType } from '../../redirect-page/redirect-struct'; +import { StorageService } from '../../../services/storage.service'; +import { ConfirmData } from '../payment-confirm/confirm.data'; class TestPayment { - constructor(public referenceName: string, public purpose: string) { - } + constructor(public referenceName: string, public purpose: string) {} } - @Component({ selector: 'app-initiate', templateUrl: './initiate.component.html', @@ -25,11 +23,11 @@ export class InitiateComponent implements OnInit { public static ROUTE = 'initiate'; static TEST_PAYMENTS: TestPayment[] = [ - new TestPayment("test user", "test transfer"), - new TestPayment("Anton", "Transfer to Anton (demo)"), - new TestPayment("Amazon payment", "Payment for order #12345 (demo)"), - new TestPayment("Apple", "Apple ITunes payment (demo)"), - new TestPayment("Netflix TV", "Netflix payment (demo)") + new TestPayment('test user', 'test transfer'), + new TestPayment('Anton', 'Transfer to Anton (demo)'), + new TestPayment('Amazon payment', 'Payment for order #12345 (demo)'), + new TestPayment('Apple', 'Apple ITunes payment (demo)'), + new TestPayment('Netflix TV', 'Netflix payment (demo)') ]; bankId = ''; @@ -52,7 +50,8 @@ export class InitiateComponent implements OnInit { ngOnInit() { this.debtorIban = this.debtorIban ? this.debtorIban : this.getDebitorIban(this.accountId); - const testPayment = InitiateComponent.TEST_PAYMENTS[Math.floor(Math.random() * InitiateComponent.TEST_PAYMENTS.length)] + const testPayment = + InitiateComponent.TEST_PAYMENTS[Math.floor(Math.random() * InitiateComponent.TEST_PAYMENTS.length)]; this.paymentForm = this.formBuilder.group({ name: [testPayment.referenceName, Validators.required], creditorIban: ['AL90208110080000001039531801', [ValidatorService.validateIban, Validators.required]], diff --git a/fintech-examples/fintech-ui/src/app/bank/services/ais.service.spec.ts b/fintech-examples/fintech-ui/src/app/bank/services/ais.service.spec.ts index 2422f9fab3..7a968c541c 100644 --- a/fintech-examples/fintech-ui/src/app/bank/services/ais.service.spec.ts +++ b/fintech-examples/fintech-ui/src/app/bank/services/ais.service.spec.ts @@ -26,13 +26,20 @@ describe('AisService', () => { it('should get accounts', () => { const getAccountsSpy = spyOn(aisService, 'getAccounts'); - aisService.getAccounts('1234', LoARetrievalInformation.FROM_TPP_WITH_AVAILABLE_CONSENT, true, false); + aisService.getAccounts('1234', LoARetrievalInformation.FROM_TPP_WITH_AVAILABLE_CONSENT, '', true, false, true); expect(getAccountsSpy).toHaveBeenCalled(); }); it('should get transactions', () => { const getTransactionsSpy = spyOn(aisService, 'getTransactions'); - aisService.getTransactions('1234', 'xxxxxxxxxx', LoTRetrievalInformation.FROM_TPP_WITH_AVAILABLE_CONSENT, false); + aisService.getTransactions( + '1234', + 'xxxxxxxxxx', + LoTRetrievalInformation.FROM_TPP_WITH_AVAILABLE_CONSENT, + '', + false, + true + ); expect(getTransactionsSpy).toHaveBeenCalled(); }); }); diff --git a/fintech-examples/fintech-ui/src/app/bank/services/ais.service.ts b/fintech-examples/fintech-ui/src/app/bank/services/ais.service.ts index 78b467cac3..c5b8aee4ee 100644 --- a/fintech-examples/fintech-ui/src/app/bank/services/ais.service.ts +++ b/fintech-examples/fintech-ui/src/app/bank/services/ais.service.ts @@ -12,7 +12,14 @@ export class AisService { return toConvert.toISOString().split('T')[0]; } - getAccounts(bankId: string, loARetrievalInformation: LoARetrievalInformation, withBalance: boolean, online: boolean) { + getAccounts( + bankId: string, + loARetrievalInformation: LoARetrievalInformation, + createConsentIfNone: string, + withBalance: boolean, + online: boolean, + authenticatePsu: boolean + ) { const okurl = window.location.pathname; const notOkUrl = okurl.replace(/account.*/, ''); @@ -23,6 +30,8 @@ export class AisService { okurl, notOkUrl, loARetrievalInformation, + authenticatePsu, + createConsentIfNone, withBalance, online, 'response' @@ -33,7 +42,9 @@ export class AisService { bankId: string, accountId: string, loTRetrievalInformation: LoTRetrievalInformation, - online: boolean + createConsentIfNone: string, + online: boolean, + authenticatePsu: boolean ) { const okurl = window.location.pathname; const notOkUrl = okurl.replace(/account.*/, 'accounts'); @@ -46,6 +57,8 @@ export class AisService { okurl, notOkUrl, loTRetrievalInformation, + authenticatePsu, + createConsentIfNone, '1970-01-01', AisService.isoDate(new Date()), null, diff --git a/fintech-examples/fintech-ui/src/app/bank/settings/settings.component.html b/fintech-examples/fintech-ui/src/app/bank/settings/settings.component.html index 1846adb164..cc6a48ef41 100644 --- a/fintech-examples/fintech-ui/src/app/bank/settings/settings.component.html +++ b/fintech-examples/fintech-ui/src/app/bank/settings/settings.component.html @@ -43,7 +43,7 @@

List of Transactions

- +
List of Transactions />
+
+ + +
+ + +
+

Consent Request

+
+
+ + + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
@@ -68,8 +154,13 @@

Payments

+
+

Other settings

+ +
-