From e8bf52dee75d6e12e7972f01e8c70e6891e6325b Mon Sep 17 00:00:00 2001 From: andrej romanov <50377758+auumgn@users.noreply.github.com> Date: Tue, 26 Mar 2024 14:56:57 +0200 Subject: [PATCH 1/3] add landing page content --- ui/package-lock.json | 152 ++++++---- ui/package.json | 3 + ui/src/app/app.module.ts | 3 - .../landing-page/landing-page.component.html | 71 +++++ .../landing-page.component.spec.ts | 21 ++ .../landing-page/landing-page.component.ts | 269 ++++++++++++++++++ .../app/landing-page/landing-page.module.ts | 25 ++ ui/src/app/landing-page/landing-page.route.ts | 11 + .../app/landing-page/landing-page.service.ts | 48 ++++ 9 files changed, 546 insertions(+), 57 deletions(-) create mode 100644 ui/src/app/landing-page/landing-page.component.html create mode 100644 ui/src/app/landing-page/landing-page.component.spec.ts create mode 100644 ui/src/app/landing-page/landing-page.component.ts create mode 100644 ui/src/app/landing-page/landing-page.module.ts create mode 100644 ui/src/app/landing-page/landing-page.route.ts create mode 100644 ui/src/app/landing-page/landing-page.service.ts diff --git a/ui/package-lock.json b/ui/package-lock.json index d74369555..744e83d18 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -22,6 +22,8 @@ "@fortawesome/free-solid-svg-icons": "^6.4.2", "@ng-bootstrap/ng-bootstrap": "^15.1.1", "bootstrap": "^5.3.2", + "jsrsasign": "^11.1.0", + "jsrsasign-util": "^1.0.5", "moment": "^2.29.4", "ngx-clipboard": "^16.0.0", "ngx-webstorage": "^12.0.0", @@ -39,6 +41,7 @@ "@angular/cli": "~16.2.6", "@angular/compiler-cli": "^16.2.9", "@types/jasmine": "~4.0.0", + "@types/jsrsasign": "^10.5.13", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", "eslint": "^8.49.0", @@ -4864,6 +4867,12 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/jsrsasign": { + "version": "10.5.13", + "resolved": "https://registry.npmjs.org/@types/jsrsasign/-/jsrsasign-10.5.13.tgz", + "integrity": "sha512-vvVHLrXxoUZgBWTcJnTMSC4FAQcG2loK7N1Uy20I3nr/aUhetbGdfuwSzXkrMoll2RoYKW0IcMIN0I0bwMwVMQ==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", @@ -6015,13 +6024,13 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -6029,7 +6038,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -6600,9 +6609,9 @@ } }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "engines": { "node": ">= 0.6" @@ -8071,17 +8080,17 @@ "dev": true }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -8119,9 +8128,9 @@ "dev": true }, "node_modules/express/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, "engines": { "node": ">= 0.6" @@ -8406,9 +8415,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -9924,8 +9933,7 @@ "node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "node_modules/jsonfile": { "version": "4.0.0", @@ -9945,6 +9953,23 @@ "node >= 0.2.0" ] }, + "node_modules/jsrsasign": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.0.tgz", + "integrity": "sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==", + "funding": { + "url": "https://github.com/kjur/jsrsasign#donations" + } + }, + "node_modules/jsrsasign-util": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/jsrsasign-util/-/jsrsasign-util-1.0.5.tgz", + "integrity": "sha512-e5Kp8aaT5GH2c5X8j4uaJruYmT4GcnaGb47nw8m60YqPywtnOtTISZ9hZgtZ3a+jh7B27bU2LCf3Y32wZyfhtQ==", + "dependencies": { + "jsonc-parser": ">= 0.0.1", + "jsrsasign": ">= 4.8.2" + } + }, "node_modules/karma": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.1.tgz", @@ -12753,9 +12778,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -14913,9 +14938,9 @@ } }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "dev": true, "dependencies": { "colorette": "^2.0.10", @@ -18855,6 +18880,12 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "@types/jsrsasign": { + "version": "10.5.13", + "resolved": "https://registry.npmjs.org/@types/jsrsasign/-/jsrsasign-10.5.13.tgz", + "integrity": "sha512-vvVHLrXxoUZgBWTcJnTMSC4FAQcG2loK7N1Uy20I3nr/aUhetbGdfuwSzXkrMoll2RoYKW0IcMIN0I0bwMwVMQ==", + "dev": true + }, "@types/mime": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", @@ -19730,13 +19761,13 @@ } }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -19744,7 +19775,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -20170,9 +20201,9 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true }, "convert-source-map": { @@ -21262,17 +21293,17 @@ "dev": true }, "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -21307,9 +21338,9 @@ "dev": true }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true }, "debug": { @@ -21546,9 +21577,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true }, "foreground-child": { @@ -22655,8 +22686,7 @@ "jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "jsonfile": { "version": "4.0.0", @@ -22673,6 +22703,20 @@ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "dev": true }, + "jsrsasign": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.0.tgz", + "integrity": "sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==" + }, + "jsrsasign-util": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/jsrsasign-util/-/jsrsasign-util-1.0.5.tgz", + "integrity": "sha512-e5Kp8aaT5GH2c5X8j4uaJruYmT4GcnaGb47nw8m60YqPywtnOtTISZ9hZgtZ3a+jh7B27bU2LCf3Y32wZyfhtQ==", + "requires": { + "jsonc-parser": ">= 0.0.1", + "jsrsasign": ">= 4.8.2" + } + }, "karma": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.1.tgz", @@ -24743,9 +24787,9 @@ "dev": true }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "requires": { "bytes": "3.1.2", @@ -26350,9 +26394,9 @@ }, "dependencies": { "webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "dev": true, "requires": { "colorette": "^2.0.10", diff --git a/ui/package.json b/ui/package.json index 8eae3dc40..a3ddf84ac 100644 --- a/ui/package.json +++ b/ui/package.json @@ -28,6 +28,8 @@ "@fortawesome/free-solid-svg-icons": "^6.4.2", "@ng-bootstrap/ng-bootstrap": "^15.1.1", "bootstrap": "^5.3.2", + "jsrsasign": "^11.1.0", + "jsrsasign-util": "^1.0.5", "moment": "^2.29.4", "ngx-clipboard": "^16.0.0", "ngx-webstorage": "^12.0.0", @@ -45,6 +47,7 @@ "@angular/cli": "~16.2.6", "@angular/compiler-cli": "^16.2.9", "@types/jasmine": "~4.0.0", + "@types/jsrsasign": "^10.5.13", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", "eslint": "^8.49.0", diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 7f416c348..70605bbe8 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -6,10 +6,8 @@ import { AppComponent } from './app.component' import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http' import { AccountModule } from './account/account.module' import { NgxWebstorageModule } from 'ngx-webstorage' -import { FontAwesomeModule } from '@fortawesome/angular-fontawesome' import { NavbarComponent } from './layout/navbar/navbar.component' import { CommonModule } from '@angular/common' -import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { HomeModule } from './home/home.module' import { FooterComponent } from './layout/footer/footer.component' import { SharedModule } from './shared/shared.module' @@ -18,7 +16,6 @@ import { ErrorService } from './error/service/error.service' import { ErrorComponent } from './error/error.component' import { FormsModule } from '@angular/forms' import { UserModule } from './user/user.module' -import { AffiliationsComponent } from './affiliation/affiliations.component' import { AffiliationModule } from './affiliation/affiliation.module' @NgModule({ diff --git a/ui/src/app/landing-page/landing-page.component.html b/ui/src/app/landing-page/landing-page.component.html new file mode 100644 index 000000000..3d654fb5a --- /dev/null +++ b/ui/src/app/landing-page/landing-page.component.html @@ -0,0 +1,71 @@ +
+
+
+
+
+ Loading... +
+
+
+
+
+

+ You have already granted
+ {{ clientName }}
+ permission to update your ORCID record +

+

+ + {{ issuer }}/{{ orcidRecord.orcid }} +

+

+ {{ incorrectDataMessage }} +

+
+
+

+ {{ linkAlreadyUsedMessage }} +

+
+
+

+ Oops, something went wrong and we were not able to fetch your ORCID iD +

+
+
+

+ Oops, you have denied access.
+ {{ clientName }}
+ will not be able to update your ORCID record. +

+

+ If this was a mistake, click the button below to grant access. +

+ + {{ allowToUpdateRecordMessage }} + +
+
+

+ {{ thanksMessage }} +

+

+ {{ successfullyGrantedMessage }} +

+

+ + {{ issuer }}/{{ signedInIdToken.sub }} +

+

+ {{ incorrectDataMessage }} +

+
+
+
+
+
+
diff --git a/ui/src/app/landing-page/landing-page.component.spec.ts b/ui/src/app/landing-page/landing-page.component.spec.ts new file mode 100644 index 000000000..5b7ad0b17 --- /dev/null +++ b/ui/src/app/landing-page/landing-page.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LandingPageComponent } from './landing-page.component'; + +describe('LandingPageComponent', () => { + let component: LandingPageComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [LandingPageComponent] + }); + fixture = TestBed.createComponent(LandingPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/src/app/landing-page/landing-page.component.ts b/ui/src/app/landing-page/landing-page.component.ts new file mode 100644 index 000000000..04a427fb1 --- /dev/null +++ b/ui/src/app/landing-page/landing-page.component.ts @@ -0,0 +1,269 @@ +import { Component, OnInit } from '@angular/core' +import { HttpErrorResponse, HttpResponse } from '@angular/common/http' +import { interval } from 'rxjs' +import { KEYUTIL, KJUR, RSAKey } from 'jsrsasign' +import { LandingPageService } from './landing-page.service' +import { MemberService } from '../member/service/member.service' +import { IMember } from '../member/model/member.model' +import { ORCID_BASE_URL } from '../app.constants' + +@Component({ + selector: 'landing-page', + templateUrl: './landing-page.component.html', +}) +export class LandingPageComponent implements OnInit { + issuer: string = ORCID_BASE_URL + oauthBaseUrl: string = ORCID_BASE_URL + '/oauth/authorize' + redirectUri: string = '/landing-page' + + loading: Boolean = true + showConnectionExists: Boolean = false + showConnectionExistsDifferentUser: Boolean = false + showDenied: Boolean = false + showError: Boolean = false + showSuccess: Boolean = false + key: any + clientName: string | undefined + salesforceId: string | undefined + clientId: string | undefined + orcidId: string | undefined + oauthUrl: string | undefined + orcidRecord: any + signedInIdToken: any + givenName: string | undefined + familyName: string | undefined + progressbarValue = 100 + curSec = 0 + incorrectDataMessage = '' + linkAlreadyUsedMessage = '' + allowToUpdateRecordMessage = '' + successfullyGrantedMessage = '' + thanksMessage = '' + + constructor( + private landingPageService: LandingPageService, + protected memberService: MemberService + ) {} + + ngOnInit() { + const id_token_fragment = this.getFragmentParameterByName('id_token') + const access_token_fragment = this.getFragmentParameterByName('access_token') + const state_param = this.getQueryParameterByName('state') + + if (state_param) { + this.landingPageService.getOrcidConnectionRecord(state_param).subscribe({ + next: (result: HttpResponse) => { + this.orcidRecord = result.body + this.landingPageService.getMemberInfo(state_param).subscribe({ + next: (res: IMember) => { + this.clientName = res.clientName + this.clientId = res.clientId + this.salesforceId = res.salesforceId + this.oauthUrl = + this.oauthBaseUrl + + '?response_type=token&redirect_uri=' + + this.redirectUri + + '&client_id=' + + this.clientId + + '&scope=/read-limited /activities/update /person/update openid&prompt=login&state=' + + state_param + + this.incorrectDataMessage = $localize`:@@landingPage.success.ifYouFind:If you find that data added to your ORCID record is incorrect, please contact ${this.clientName}` + this.linkAlreadyUsedMessage = $localize`:@@landingPage.connectionExists.differentUser.string:This authorization link has already been used. Please contact ${this.clientName} for a new authorization link.` + this.allowToUpdateRecordMessage = $localize`:@@landingPage.denied.grantAccess.string:Allow ${this.clientName} to update my ORCID record.` + this.successfullyGrantedMessage = $localize`:@@landingPage.success.youHaveSuccessfully.string:You have successfully granted ${this.clientName} permission to update your ORCID record, and your record has been updated with affiliation information.` + + // Check if id token exists in URL (user just granted permission) + if (id_token_fragment != null && id_token_fragment !== '') { + this.checkSubmitToken(id_token_fragment, state_param, access_token_fragment) + } else { + const error = this.getFragmentParameterByName('error') + // Check if user denied permission + if (error != null && error !== '') { + if (error === 'access_denied') { + this.submitUserDenied(state_param) + } else { + this.showErrorElement() + } + } else { + window.location.replace(this.oauthUrl) + } + } + + this.startTimer(600) + }, + error: (res: HttpErrorResponse) => { + console.log('error') + }, + }) + }, + error: (res: HttpErrorResponse) => { + console.log('error') + }, + }) + } + } + + getFragmentParameterByName(name: string): string { + name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]') + const regex = new RegExp('[\\#&]' + name + '=([^&#]*)'), + results = regex.exec(window.location.hash) + return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')) + } + + getQueryParameterByName(name: string): string | null { + name = name.replace(/[\[\]]/g, '\\$&') + const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), + results = regex.exec(window.location.href) + if (!results) { + return null + } + if (!results[2]) { + return '' + } + return decodeURIComponent(results[2].replace(/\+/g, ' ')) + } + + checkSubmitToken(id_token: string, state: string, access_token: string) { + this.landingPageService.getPublicKey().subscribe( + (res) => { + const pubKey = KEYUTIL.getKey(res.keys[0]) as RSAKey + const response = KJUR.jws.JWS.verifyJWT(id_token, pubKey, { + alg: ['RS256'], + iss: [this.issuer], + aud: [this.clientId || ''], + gracePeriod: 15 * 60, // 15 mins skew allowed + }) + if (response === true) { + // check if existing token belongs to a different user + + this.landingPageService.submitUserResponse({ id_token, state, salesforce_id: this.salesforceId }).subscribe({ + next: (res) => { + const data = res + if (data) { + if (data.isDifferentUser) { + this.showConnectionExistsDifferentUserElement() + return + } + if (data.isSameUserThatAlreadyGranted) { + this.showConnectionExistsElement() + return + } + } + this.landingPageService.getUserInfo(access_token).subscribe({ + next: (result: HttpResponse) => { + this.signedInIdToken = result + this.givenName = '' + if (this.signedInIdToken.given_name) { + this.givenName = this.signedInIdToken.given_name + } + this.familyName = '' + if (this.signedInIdToken.family_name) { + this.familyName = this.signedInIdToken.family_name + } + this.thanksMessage = $localize`:@@landingPage.success.thanks.string:Thanks, ${this.givenName} ${this.familyName}!` + + this.showSuccessElement() + }, + error: () => { + this.showErrorElement() + }, + }) + }, + error: () => { + this.showErrorElement() + }, + }) + } else { + this.showErrorElement() + } + }, + () => { + this.showErrorElement() + } + ) + } + + submitIdTokenData(id_token: string, state: string, access_token: string) { + this.landingPageService.submitUserResponse({ id_token, state }).subscribe({ + next: () => { + this.landingPageService.getUserInfo(access_token).subscribe({ + next: (res: HttpResponse) => { + this.signedInIdToken = res + this.showSuccessElement() + }, + error: () => { + this.showErrorElement() + }, + }) + }, + error: () => { + this.showErrorElement() + }, + }) + } + + submitUserDenied(state: string) { + this.landingPageService.submitUserResponse({ denied: true, state }).subscribe( + () => { + this.showDeniedElement() + }, + () => { + this.showErrorElement() + } + ) + } + + startTimer(seconds: number) { + const timer = interval(100) + const sub = timer.subscribe((sec) => { + this.progressbarValue = (sec * 100) / seconds + this.curSec = sec + if (this.curSec === seconds) { + sub.unsubscribe() + } + }) + } + + showConnectionExistsElement(): void { + this.showDenied = false + this.showError = false + this.showSuccess = false + this.showConnectionExists = true + this.loading = false + this.showConnectionExistsDifferentUser = false + } + + showConnectionExistsDifferentUserElement(): void { + this.showDenied = false + this.showError = false + this.showSuccess = false + this.showConnectionExists = false + this.loading = false + this.showConnectionExistsDifferentUser = true + } + + showErrorElement(): void { + this.showDenied = false + this.showError = true + this.showSuccess = false + this.loading = false + this.showConnectionExistsDifferentUser = false + } + + showDeniedElement(): void { + this.showDenied = true + this.showError = false + this.showSuccess = false + this.loading = false + this.showConnectionExistsDifferentUser = false + } + + showSuccessElement(): void { + this.showDenied = false + this.showError = false + this.showSuccess = true + this.loading = false + this.showConnectionExistsDifferentUser = false + } +} diff --git a/ui/src/app/landing-page/landing-page.module.ts b/ui/src/app/landing-page/landing-page.module.ts new file mode 100644 index 000000000..1f98b5429 --- /dev/null +++ b/ui/src/app/landing-page/landing-page.module.ts @@ -0,0 +1,25 @@ +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' +import { RouterModule } from '@angular/router' + +import { BrowserModule } from '@angular/platform-browser' + +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' + +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' +import { MatProgressBarModule } from '@angular/material/progress-bar' +import { LandingPageComponent } from './landing-page.component' +import { LANDING_PAGE_ROUTE } from './landing-page.route' + +@NgModule({ + imports: [ + RouterModule.forChild([LANDING_PAGE_ROUTE]), + BrowserModule, + BrowserAnimationsModule, + + MatProgressSpinnerModule, + MatProgressBarModule, + ], + declarations: [LandingPageComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class GatewayLandingPageModule {} diff --git a/ui/src/app/landing-page/landing-page.route.ts b/ui/src/app/landing-page/landing-page.route.ts new file mode 100644 index 000000000..9ffdc5843 --- /dev/null +++ b/ui/src/app/landing-page/landing-page.route.ts @@ -0,0 +1,11 @@ +import { Route } from '@angular/router' +import { LandingPageComponent } from './landing-page.component' + +export const LANDING_PAGE_ROUTE: Route = { + path: 'landing-page', + component: LandingPageComponent, + data: { + authorities: [], + pageTitle: 'landingPage.title.string', + }, +} diff --git a/ui/src/app/landing-page/landing-page.service.ts b/ui/src/app/landing-page/landing-page.service.ts new file mode 100644 index 000000000..6758e7289 --- /dev/null +++ b/ui/src/app/landing-page/landing-page.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core' +import { HttpClient, HttpClientModule, HttpHeaders } from '@angular/common/http' +import { Observable } from 'rxjs' +import { ORCID_BASE_URL } from '../app.constants' + +@Injectable({ providedIn: 'root' }) +export class LandingPageService { + private headers: HttpHeaders + + idTokenUri: string = '/services/assertionservice/api/id-token' + recordConnectionUri: string = '/services/assertionservice/api/assertion/record/' + memberInfoUri: string = '/services/memberservice/api/members/authorized/' + userInfoUri: string = ORCID_BASE_URL + '/oauth/userinfo' + publicKeyUri: string = ORCID_BASE_URL + '/oauth/jwks' + + constructor(private http: HttpClient) { + this.headers = new HttpHeaders({ + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + }) + } + + submitUserResponse(data: any): Observable { + return this.http.post(this.idTokenUri, JSON.stringify(data), { headers: this.headers }) + } + + getOrcidConnectionRecord(state: String): Observable { + const requestUrl = this.recordConnectionUri + state + return this.http.get(requestUrl, { observe: 'response' }) + } + + getMemberInfo(state: String): Observable { + const requestUrl = this.memberInfoUri + state + return this.http.get(requestUrl) + } + + getUserInfo(access_token: String): Observable { + const headers = new HttpHeaders({ + Authorization: 'Bearer ' + access_token, + 'Content-Type': 'application/json', + }) + return this.http.post(this.userInfoUri, {}, { headers }) + } + + getPublicKey(): Observable { + return this.http.get(this.publicKeyUri) + } +} From 61bdf5ea92c60493a5099018e68436cf0f064f37 Mon Sep 17 00:00:00 2001 From: andrej romanov <50377758+auumgn@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:40:19 +0200 Subject: [PATCH 2/3] make the landing page work also correct all route paths --- ui/src/app/affiliation/affiliation.route.ts | 4 ++-- ui/src/app/app-routing.module.ts | 8 +++++--- .../app/landing-page/landing-page.component.ts | 2 +- ui/src/app/landing-page/landing-page.module.ts | 13 ++----------- ui/src/app/landing-page/landing-page.route.ts | 18 ++++++++++-------- ui/src/app/shared/shared.module.ts | 2 +- ui/src/app/user/user.route.ts | 8 ++++---- ui/src/app/user/users.component.spec.ts | 6 +++++- 8 files changed, 30 insertions(+), 31 deletions(-) diff --git a/ui/src/app/affiliation/affiliation.route.ts b/ui/src/app/affiliation/affiliation.route.ts index d186e48d2..b3fae9ab4 100644 --- a/ui/src/app/affiliation/affiliation.route.ts +++ b/ui/src/app/affiliation/affiliation.route.ts @@ -28,7 +28,7 @@ export const AffiliationResolver: ResolveFn = ( export const affiliationRoutes: Routes = [ { - path: 'affiliations', + path: '', component: AffiliationsComponent, data: { authorities: ['ASSERTION_SERVICE_ENABLED'], @@ -76,7 +76,7 @@ export const affiliationRoutes: Routes = [ ], }, { - path: 'affiliations/:id/view', + path: ':id/view', component: AffiliationDetailComponent, resolve: { affiliation: AffiliationResolver, diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index e97de382e..b3c2fb2da 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -1,9 +1,7 @@ import { NgModule } from '@angular/core' -import { ActivatedRouteSnapshot, Resolve, Route, RouterModule, RouterStateSnapshot, Routes } from '@angular/router' +import { RouterModule, Routes } from '@angular/router' import { navbarRoute } from './layout/navbar/navbar.route' import { errorRoutes } from './error/error.route' -import { AuthGuard } from './account/auth.guard' -import { UsersComponent } from './user/users.component' const routes: Routes = [ { @@ -22,6 +20,10 @@ const routes: Routes = [ path: 'affiliations', loadChildren: () => import('./affiliation/affiliation.module').then((m) => m.AffiliationModule), }, + { + path: 'landing-page', + loadChildren: () => import('./landing-page/landing-page.module').then((m) => m.LandingPageModule), + }, ] @NgModule({ diff --git a/ui/src/app/landing-page/landing-page.component.ts b/ui/src/app/landing-page/landing-page.component.ts index 04a427fb1..7f0d21dae 100644 --- a/ui/src/app/landing-page/landing-page.component.ts +++ b/ui/src/app/landing-page/landing-page.component.ts @@ -8,7 +8,7 @@ import { IMember } from '../member/model/member.model' import { ORCID_BASE_URL } from '../app.constants' @Component({ - selector: 'landing-page', + selector: 'app-landing-page', templateUrl: './landing-page.component.html', }) export class LandingPageComponent implements OnInit { diff --git a/ui/src/app/landing-page/landing-page.module.ts b/ui/src/app/landing-page/landing-page.module.ts index 1f98b5429..6af3902e6 100644 --- a/ui/src/app/landing-page/landing-page.module.ts +++ b/ui/src/app/landing-page/landing-page.module.ts @@ -1,8 +1,6 @@ import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' import { RouterModule } from '@angular/router' -import { BrowserModule } from '@angular/platform-browser' - import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' @@ -11,15 +9,8 @@ import { LandingPageComponent } from './landing-page.component' import { LANDING_PAGE_ROUTE } from './landing-page.route' @NgModule({ - imports: [ - RouterModule.forChild([LANDING_PAGE_ROUTE]), - BrowserModule, - BrowserAnimationsModule, - - MatProgressSpinnerModule, - MatProgressBarModule, - ], + imports: [MatProgressSpinnerModule, MatProgressBarModule, RouterModule.forChild(LANDING_PAGE_ROUTE)], declarations: [LandingPageComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) -export class GatewayLandingPageModule {} +export class LandingPageModule {} diff --git a/ui/src/app/landing-page/landing-page.route.ts b/ui/src/app/landing-page/landing-page.route.ts index 9ffdc5843..cd7099ccf 100644 --- a/ui/src/app/landing-page/landing-page.route.ts +++ b/ui/src/app/landing-page/landing-page.route.ts @@ -1,11 +1,13 @@ -import { Route } from '@angular/router' +import { Routes } from '@angular/router' import { LandingPageComponent } from './landing-page.component' -export const LANDING_PAGE_ROUTE: Route = { - path: 'landing-page', - component: LandingPageComponent, - data: { - authorities: [], - pageTitle: 'landingPage.title.string', +export const LANDING_PAGE_ROUTE: Routes = [ + { + path: '', + component: LandingPageComponent, + data: { + authorities: [], + pageTitle: 'landingPage.title.string', + }, }, -} +] diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index 078ba4194..e3c1cf79b 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -9,7 +9,7 @@ import { AlertComponent } from './alert/alert.component' import { LocalizePipe } from './pipe/localize' @NgModule({ - imports: [CommonModule, NgbModule, FontAwesomeModule], + imports: [NgbModule, FontAwesomeModule], declarations: [FindLanguageFromKeyPipe, LocalizePipe, ErrorAlertComponent, HasAnyAuthorityDirective, AlertComponent], exports: [ FindLanguageFromKeyPipe, diff --git a/ui/src/app/user/user.route.ts b/ui/src/app/user/user.route.ts index ddb774385..ee53eb104 100644 --- a/ui/src/app/user/user.route.ts +++ b/ui/src/app/user/user.route.ts @@ -27,7 +27,7 @@ export const UserResolver: ResolveFn = ( export const routes: Routes = [ { - path: 'users', + path: '', component: UsersComponent, data: { authorities: ['ROLE_ADMIN', 'ROLE_ORG_OWNER', 'ROLE_CONSORTIUM_LEAD'], @@ -66,7 +66,7 @@ export const routes: Routes = [ ], }, { - path: 'users/:id/view', + path: ':id/view', component: UserDetailComponent, resolve: { user: UserResolver, @@ -78,7 +78,7 @@ export const routes: Routes = [ canActivate: [AuthGuard], }, { - path: 'users/new', + path: 'new', component: UserUpdateComponent, resolve: { user: UserResolver, @@ -90,7 +90,7 @@ export const routes: Routes = [ canActivate: [AuthGuard], }, { - path: 'users/:id/edit', + path: ':id/edit', component: UserUpdateComponent, resolve: { user: UserResolver, diff --git a/ui/src/app/user/users.component.spec.ts b/ui/src/app/user/users.component.spec.ts index a3a19957c..e33bfa34b 100644 --- a/ui/src/app/user/users.component.spec.ts +++ b/ui/src/app/user/users.component.spec.ts @@ -42,7 +42,11 @@ describe('UsersComponent', () => { TestBed.configureTestingModule({ declarations: [UsersComponent, HasAnyAuthorityDirective, LocalizePipe], - imports: [ReactiveFormsModule, RouterModule.forRoot([{ path: 'users', component: UsersComponent }]), FormsModule], + imports: [ + ReactiveFormsModule, + RouterModule.forChild([{ path: 'users', component: UsersComponent }]), + FormsModule, + ], providers: [ { provide: UserService, useValue: userServiceSpy }, { provide: AccountService, useValue: accountServiceSpy }, From ca669077758a3c45fa3c812032979c4b5be99f17 Mon Sep 17 00:00:00 2001 From: andrej romanov <50377758+auumgn@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:21:23 +0200 Subject: [PATCH 3/3] add unit tests and final tweaks --- .../landing-page/landing-page.component.html | 4 +- .../landing-page.component.spec.ts | 153 ++++++++++++++++-- .../landing-page/landing-page.component.ts | 151 ++++++++--------- .../app/landing-page/landing-page.module.ts | 3 +- .../app/landing-page/landing-page.service.ts | 21 +-- ui/src/app/shared/model/orcid-record.model.ts | 15 ++ .../shared/service/window-location.service.ts | 4 + ui/src/app/user/users.component.spec.ts | 1 + 8 files changed, 244 insertions(+), 108 deletions(-) create mode 100644 ui/src/app/shared/model/orcid-record.model.ts diff --git a/ui/src/app/landing-page/landing-page.component.html b/ui/src/app/landing-page/landing-page.component.html index 3d654fb5a..03715968c 100644 --- a/ui/src/app/landing-page/landing-page.component.html +++ b/ui/src/app/landing-page/landing-page.component.html @@ -17,7 +17,7 @@

>

- + orcid-logo {{ issuer }}/{{ orcidRecord.orcid }}

@@ -57,7 +57,7 @@

{{ successfullyGrantedMessage }}

- + orcid-logo {{ issuer }}/{{ signedInIdToken.sub }}

diff --git a/ui/src/app/landing-page/landing-page.component.spec.ts b/ui/src/app/landing-page/landing-page.component.spec.ts index 5b7ad0b17..42e67adaa 100644 --- a/ui/src/app/landing-page/landing-page.component.spec.ts +++ b/ui/src/app/landing-page/landing-page.component.spec.ts @@ -1,21 +1,146 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { LandingPageComponent } from './landing-page.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { HttpClientModule } from '@angular/common/http' +import { LandingPageComponent } from './landing-page.component' +import { LandingPageService } from './landing-page.service' +import { OrcidRecord } from '../shared/model/orcid-record.model' +import { of } from 'rxjs' +import { Member } from '../member/model/member.model' +import { WindowLocationService } from '../shared/service/window-location.service' +import * as KEYUTIL from 'jsrsasign' describe('LandingPageComponent', () => { - let component: LandingPageComponent; - let fixture: ComponentFixture; + let component: LandingPageComponent + let fixture: ComponentFixture + let landingPageService: jasmine.SpyObj + let windowLocationService: jasmine.SpyObj beforeEach(() => { TestBed.configureTestingModule({ - declarations: [LandingPageComponent] - }); - fixture = TestBed.createComponent(LandingPageComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + declarations: [LandingPageComponent], + imports: [HttpClientModule], + providers: [ + { + provide: LandingPageService, + useValue: jasmine.createSpyObj('LandingPageService', [ + 'getOrcidConnectionRecord', + 'getMemberInfo', + 'getPublicKey', + 'submitUserResponse', + 'getUserInfo', + 'submitUserResponse', + ]), + }, + { + provide: WindowLocationService, + useValue: jasmine.createSpyObj('WindowLocationService', ['updateWindowLocation', 'getWindowLocationHash']), + }, + ], + }) + fixture = TestBed.createComponent(LandingPageComponent) + component = fixture.componentInstance + landingPageService = TestBed.inject(LandingPageService) as jasmine.SpyObj + windowLocationService = TestBed.inject(WindowLocationService) as jasmine.SpyObj + }) it('should create', () => { - expect(component).toBeTruthy(); - }); -}); + expect(component).toBeTruthy() + }) + + it('New record connection should redirect to the registry', () => { + windowLocationService.updateWindowLocation.and.returnValue() + landingPageService.getOrcidConnectionRecord.and.returnValue(of(new OrcidRecord('email', 'orcid'))) + landingPageService.getMemberInfo.and.returnValue( + of(new Member('id', 'name', 'email', 'orcid', 'salesforceId', 'clientId')) + ) + component.processRequest('someState', '', '') + expect(landingPageService.getOrcidConnectionRecord).toHaveBeenCalled() + expect(component.oauthUrl).toBe( + 'localhost.orcid.org/oauth/authorize?response_type=token&redirect_uri=/landing-page&client_id=name&scope=/read-limited /activities/update /person/update openid&prompt=login&state=someState' + ) + expect(landingPageService.getPublicKey).toHaveBeenCalledTimes(0) + expect(windowLocationService.updateWindowLocation).toHaveBeenCalled() + }) + + it('New record connection should fail (user denied permission)', () => { + windowLocationService.updateWindowLocation.and.returnValue() + windowLocationService.getWindowLocationHash.and.returnValue('#error=access_denied') + landingPageService.getOrcidConnectionRecord.and.returnValue(of(new OrcidRecord('email', 'orcid'))) + landingPageService.getMemberInfo.and.returnValue( + of(new Member('id', 'name', 'email', 'orcid', 'salesforceId', 'clientId')) + ) + landingPageService.submitUserResponse.and.returnValue(of('')) + component.processRequest('someState', '', '') + expect(landingPageService.getOrcidConnectionRecord).toHaveBeenCalled() + expect(component.oauthUrl).toBe( + 'localhost.orcid.org/oauth/authorize?response_type=token&redirect_uri=/landing-page&client_id=name&scope=/read-limited /activities/update /person/update openid&prompt=login&state=someState' + ) + expect(landingPageService.getPublicKey).toHaveBeenCalledTimes(0) + expect(windowLocationService.updateWindowLocation).toHaveBeenCalledTimes(0) + expect(landingPageService.submitUserResponse).toHaveBeenCalled() + expect(component.showError).toBeFalsy() + expect(component.showDenied).toBeTruthy() + }) + + it('New record connection should fail (generic error)', () => { + windowLocationService.updateWindowLocation.and.returnValue() + windowLocationService.getWindowLocationHash.and.returnValue('#error=123') + landingPageService.getOrcidConnectionRecord.and.returnValue(of(new OrcidRecord('email', 'orcid'))) + landingPageService.getMemberInfo.and.returnValue( + of(new Member('id', 'name', 'email', 'orcid', 'salesforceId', 'clientId')) + ) + landingPageService.submitUserResponse.and.returnValue(of('')) + component.processRequest('someState', '', '') + expect(landingPageService.getOrcidConnectionRecord).toHaveBeenCalled() + expect(component.oauthUrl).toBe( + 'localhost.orcid.org/oauth/authorize?response_type=token&redirect_uri=/landing-page&client_id=name&scope=/read-limited /activities/update /person/update openid&prompt=login&state=someState' + ) + expect(landingPageService.getPublicKey).toHaveBeenCalledTimes(0) + expect(windowLocationService.updateWindowLocation).toHaveBeenCalledTimes(0) + expect(landingPageService.submitUserResponse).toHaveBeenCalledTimes(0) + expect(component.showError).toBeTruthy() + expect(component.showDenied).toBeFalsy() + }) + + it('Existing record connection should be identified', () => { + windowLocationService.updateWindowLocation.and.returnValue() + landingPageService.getOrcidConnectionRecord.and.returnValue(of(new OrcidRecord('email', 'orcid'))) + landingPageService.getMemberInfo.and.returnValue( + of(new Member('id', 'name', 'email', 'orcid', 'salesforceId', 'clientId')) + ) + landingPageService.getPublicKey.and.returnValue(of(['publicKey'])) + landingPageService.submitUserResponse.and.returnValue(of('')) + landingPageService.getUserInfo.and.returnValue(of({ givenName: 'givenName', familyName: 'familyName' })) + spyOn(KEYUTIL.KEYUTIL, 'getKey').and.returnValue(new KEYUTIL.RSAKey()) + spyOn(KEYUTIL.KJUR.jws.JWS, 'verifyJWT').and.returnValue(true) + + component.processRequest('someState', 'it_token', '') + + expect(landingPageService.getOrcidConnectionRecord).toHaveBeenCalled() + expect(landingPageService.getPublicKey).toHaveBeenCalled() + expect(windowLocationService.updateWindowLocation).toHaveBeenCalledTimes(0) + }) + + it('Check for wrong user', () => { + landingPageService.submitUserResponse.and.returnValue(of({ isDifferentUser: true })) + landingPageService.getPublicKey.and.returnValue(of(['publicKey'])) + landingPageService.getUserInfo.and.returnValue(of({ givenName: 'givenName', familyName: 'familyName' })) + spyOn(KEYUTIL.KEYUTIL, 'getKey').and.returnValue(new KEYUTIL.RSAKey()) + spyOn(KEYUTIL.KJUR.jws.JWS, 'verifyJWT').and.returnValue(true) + component.checkSubmitToken('token', 'state', 'access_token') + expect(landingPageService.submitUserResponse).toHaveBeenCalled() + expect(component.showConnectionExists).toBeFalsy() + expect(component.showConnectionExistsDifferentUser).toBeTruthy() + }) + + it('Check for existing connection', () => { + landingPageService.submitUserResponse.and.returnValue(of({ isSameUserThatAlreadyGranted: true })) + landingPageService.getPublicKey.and.returnValue(of(['publicKey'])) + landingPageService.getUserInfo.and.returnValue(of({ givenName: 'givenName', familyName: 'familyName' })) + spyOn(KEYUTIL.KEYUTIL, 'getKey').and.returnValue(new KEYUTIL.RSAKey()) + spyOn(KEYUTIL.KJUR.jws.JWS, 'verifyJWT').and.returnValue(true) + component.checkSubmitToken('token', 'state', 'access_token') + expect(landingPageService.submitUserResponse).toHaveBeenCalled() + expect(component.showConnectionExists).toBeTruthy() + expect(component.showConnectionExistsDifferentUser).toBeFalsy() + }) +}) diff --git a/ui/src/app/landing-page/landing-page.component.ts b/ui/src/app/landing-page/landing-page.component.ts index 7f0d21dae..e2294c31a 100644 --- a/ui/src/app/landing-page/landing-page.component.ts +++ b/ui/src/app/landing-page/landing-page.component.ts @@ -6,22 +6,23 @@ import { LandingPageService } from './landing-page.service' import { MemberService } from '../member/service/member.service' import { IMember } from '../member/model/member.model' import { ORCID_BASE_URL } from '../app.constants' +import { WindowLocationService } from '../shared/service/window-location.service' @Component({ selector: 'app-landing-page', templateUrl: './landing-page.component.html', }) export class LandingPageComponent implements OnInit { - issuer: string = ORCID_BASE_URL - oauthBaseUrl: string = ORCID_BASE_URL + '/oauth/authorize' - redirectUri: string = '/landing-page' + issuer = ORCID_BASE_URL + oauthBaseUrl = ORCID_BASE_URL + '/oauth/authorize' + redirectUri = '/landing-page' - loading: Boolean = true - showConnectionExists: Boolean = false - showConnectionExistsDifferentUser: Boolean = false - showDenied: Boolean = false - showError: Boolean = false - showSuccess: Boolean = false + loading = true + showConnectionExists = false + showConnectionExistsDifferentUser = false + showDenied = false + showError = false + showSuccess = false key: any clientName: string | undefined salesforceId: string | undefined @@ -42,6 +43,7 @@ export class LandingPageComponent implements OnInit { constructor( private landingPageService: LandingPageService, + private windowLocationService: WindowLocationService, protected memberService: MemberService ) {} @@ -51,67 +53,73 @@ export class LandingPageComponent implements OnInit { const state_param = this.getQueryParameterByName('state') if (state_param) { - this.landingPageService.getOrcidConnectionRecord(state_param).subscribe({ - next: (result: HttpResponse) => { - this.orcidRecord = result.body - this.landingPageService.getMemberInfo(state_param).subscribe({ - next: (res: IMember) => { - this.clientName = res.clientName - this.clientId = res.clientId - this.salesforceId = res.salesforceId - this.oauthUrl = - this.oauthBaseUrl + - '?response_type=token&redirect_uri=' + - this.redirectUri + - '&client_id=' + - this.clientId + - '&scope=/read-limited /activities/update /person/update openid&prompt=login&state=' + - state_param + this.processRequest(state_param, id_token_fragment, access_token_fragment) + } + } - this.incorrectDataMessage = $localize`:@@landingPage.success.ifYouFind:If you find that data added to your ORCID record is incorrect, please contact ${this.clientName}` - this.linkAlreadyUsedMessage = $localize`:@@landingPage.connectionExists.differentUser.string:This authorization link has already been used. Please contact ${this.clientName} for a new authorization link.` - this.allowToUpdateRecordMessage = $localize`:@@landingPage.denied.grantAccess.string:Allow ${this.clientName} to update my ORCID record.` - this.successfullyGrantedMessage = $localize`:@@landingPage.success.youHaveSuccessfully.string:You have successfully granted ${this.clientName} permission to update your ORCID record, and your record has been updated with affiliation information.` + processRequest(state_param: string, id_token_fragment: string, access_token_fragment: string) { + this.landingPageService.getOrcidConnectionRecord(state_param).subscribe({ + next: (result) => { + this.orcidRecord = result + this.landingPageService.getMemberInfo(state_param).subscribe({ + next: (res: IMember) => { + this.clientName = res.clientName + this.clientId = res.clientId + this.salesforceId = res.salesforceId + this.oauthUrl = + this.oauthBaseUrl + + '?response_type=token&redirect_uri=' + + this.redirectUri + + '&client_id=' + + this.clientId + + '&scope=/read-limited /activities/update /person/update openid&prompt=login&state=' + + state_param - // Check if id token exists in URL (user just granted permission) - if (id_token_fragment != null && id_token_fragment !== '') { - this.checkSubmitToken(id_token_fragment, state_param, access_token_fragment) - } else { - const error = this.getFragmentParameterByName('error') - // Check if user denied permission - if (error != null && error !== '') { - if (error === 'access_denied') { - this.submitUserDenied(state_param) - } else { - this.showErrorElement() - } + this.incorrectDataMessage = $localize`:@@landingPage.success.ifYouFind:If you find that data added to your ORCID record is incorrect, please contact ${this.clientName}` + this.linkAlreadyUsedMessage = $localize`:@@landingPage.connectionExists.differentUser.string:This authorization link has already been used. Please contact ${this.clientName} for a new authorization link.` + this.allowToUpdateRecordMessage = $localize`:@@landingPage.denied.grantAccess.string:Allow ${this.clientName} to update my ORCID record.` + this.successfullyGrantedMessage = $localize`:@@landingPage.success.youHaveSuccessfully.string:You have successfully granted ${this.clientName} permission to update your ORCID record, and your record has been updated with affiliation information.` + + // Check if id token exists in URL (user just granted permission) + if (id_token_fragment != null && id_token_fragment !== '') { + this.checkSubmitToken(id_token_fragment, state_param, access_token_fragment) + } else { + const error = this.getFragmentParameterByName('error') + // Check if user denied permission + if (error != null && error !== '') { + if (error === 'access_denied') { + this.submitUserDenied(state_param) } else { - window.location.replace(this.oauthUrl) + this.showErrorElement() } + } else { + this.windowLocationService.updateWindowLocation(this.oauthUrl) } + } - this.startTimer(600) - }, - error: (res: HttpErrorResponse) => { - console.log('error') - }, - }) - }, - error: (res: HttpErrorResponse) => { - console.log('error') - }, - }) - } + this.startTimer(600) + }, + error: (res: HttpErrorResponse) => { + console.log('error') + }, + }) + }, + error: (res: HttpErrorResponse) => { + console.log('error') + }, + }) } getFragmentParameterByName(name: string): string { + // eslint-disable-next-line name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]') const regex = new RegExp('[\\#&]' + name + '=([^&#]*)'), - results = regex.exec(window.location.hash) + results = regex.exec(this.windowLocationService.getWindowLocationHash()) return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')) } getQueryParameterByName(name: string): string | null { + // eslint-disable-next-line name = name.replace(/[\[\]]/g, '\\$&') const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), results = regex.exec(window.location.href) @@ -125,8 +133,8 @@ export class LandingPageComponent implements OnInit { } checkSubmitToken(id_token: string, state: string, access_token: string) { - this.landingPageService.getPublicKey().subscribe( - (res) => { + this.landingPageService.getPublicKey().subscribe({ + next: (res) => { const pubKey = KEYUTIL.getKey(res.keys[0]) as RSAKey const response = KJUR.jws.JWS.verifyJWT(id_token, pubKey, { alg: ['RS256'], @@ -178,25 +186,6 @@ export class LandingPageComponent implements OnInit { this.showErrorElement() } }, - () => { - this.showErrorElement() - } - ) - } - - submitIdTokenData(id_token: string, state: string, access_token: string) { - this.landingPageService.submitUserResponse({ id_token, state }).subscribe({ - next: () => { - this.landingPageService.getUserInfo(access_token).subscribe({ - next: (res: HttpResponse) => { - this.signedInIdToken = res - this.showSuccessElement() - }, - error: () => { - this.showErrorElement() - }, - }) - }, error: () => { this.showErrorElement() }, @@ -204,14 +193,14 @@ export class LandingPageComponent implements OnInit { } submitUserDenied(state: string) { - this.landingPageService.submitUserResponse({ denied: true, state }).subscribe( - () => { + this.landingPageService.submitUserResponse({ denied: true, state }).subscribe({ + next: () => { this.showDeniedElement() }, - () => { + error: () => { this.showErrorElement() - } - ) + }, + }) } startTimer(seconds: number) { diff --git a/ui/src/app/landing-page/landing-page.module.ts b/ui/src/app/landing-page/landing-page.module.ts index 6af3902e6..53fc89b5d 100644 --- a/ui/src/app/landing-page/landing-page.module.ts +++ b/ui/src/app/landing-page/landing-page.module.ts @@ -7,9 +7,10 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' import { MatProgressBarModule } from '@angular/material/progress-bar' import { LandingPageComponent } from './landing-page.component' import { LANDING_PAGE_ROUTE } from './landing-page.route' +import { CommonModule } from '@angular/common' @NgModule({ - imports: [MatProgressSpinnerModule, MatProgressBarModule, RouterModule.forChild(LANDING_PAGE_ROUTE)], + imports: [CommonModule, MatProgressSpinnerModule, MatProgressBarModule, RouterModule.forChild(LANDING_PAGE_ROUTE)], declarations: [LandingPageComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) diff --git a/ui/src/app/landing-page/landing-page.service.ts b/ui/src/app/landing-page/landing-page.service.ts index 6758e7289..e18311a9e 100644 --- a/ui/src/app/landing-page/landing-page.service.ts +++ b/ui/src/app/landing-page/landing-page.service.ts @@ -1,17 +1,18 @@ import { Injectable } from '@angular/core' import { HttpClient, HttpClientModule, HttpHeaders } from '@angular/common/http' -import { Observable } from 'rxjs' +import { Observable, map } from 'rxjs' import { ORCID_BASE_URL } from '../app.constants' +import { OrcidRecord } from '../shared/model/orcid-record.model' @Injectable({ providedIn: 'root' }) export class LandingPageService { private headers: HttpHeaders - idTokenUri: string = '/services/assertionservice/api/id-token' - recordConnectionUri: string = '/services/assertionservice/api/assertion/record/' - memberInfoUri: string = '/services/memberservice/api/members/authorized/' - userInfoUri: string = ORCID_BASE_URL + '/oauth/userinfo' - publicKeyUri: string = ORCID_BASE_URL + '/oauth/jwks' + idTokenUri = '/services/assertionservice/api/id-token' + recordConnectionUri = '/services/assertionservice/api/assertion/record/' + memberInfoUri = '/services/memberservice/api/members/authorized/' + userInfoUri = ORCID_BASE_URL + '/oauth/userinfo' + publicKeyUri = ORCID_BASE_URL + '/oauth/jwks' constructor(private http: HttpClient) { this.headers = new HttpHeaders({ @@ -24,17 +25,17 @@ export class LandingPageService { return this.http.post(this.idTokenUri, JSON.stringify(data), { headers: this.headers }) } - getOrcidConnectionRecord(state: String): Observable { + getOrcidConnectionRecord(state: string): Observable { const requestUrl = this.recordConnectionUri + state - return this.http.get(requestUrl, { observe: 'response' }) + return this.http.get(requestUrl).pipe(map((response: any) => response.body)) } - getMemberInfo(state: String): Observable { + getMemberInfo(state: string): Observable { const requestUrl = this.memberInfoUri + state return this.http.get(requestUrl) } - getUserInfo(access_token: String): Observable { + getUserInfo(access_token: string): Observable { const headers = new HttpHeaders({ Authorization: 'Bearer ' + access_token, 'Content-Type': 'application/json', diff --git a/ui/src/app/shared/model/orcid-record.model.ts b/ui/src/app/shared/model/orcid-record.model.ts new file mode 100644 index 000000000..a21fe3feb --- /dev/null +++ b/ui/src/app/shared/model/orcid-record.model.ts @@ -0,0 +1,15 @@ +import { Moment } from 'moment' +import { EventType } from 'src/app/app.constants' + +export class OrcidRecord { + constructor( + public email: string, + public orcid: string, + public tokens?: any, + public last_notified?: Moment, + public revoke_notification_sent_date?: Moment, + public eminder_notification_sent_date?: Moment, + public created?: Moment, + public modified?: Moment + ) {} +} diff --git a/ui/src/app/shared/service/window-location.service.ts b/ui/src/app/shared/service/window-location.service.ts index b7c93ca2f..35d980d8b 100644 --- a/ui/src/app/shared/service/window-location.service.ts +++ b/ui/src/app/shared/service/window-location.service.ts @@ -17,4 +17,8 @@ export class WindowLocationService { getWindowLocationHref(): string { return window.location.href } + + getWindowLocationHash(): string { + return window.location.hash + } } diff --git a/ui/src/app/user/users.component.spec.ts b/ui/src/app/user/users.component.spec.ts index e33bfa34b..3b0a1c70d 100644 --- a/ui/src/app/user/users.component.spec.ts +++ b/ui/src/app/user/users.component.spec.ts @@ -44,6 +44,7 @@ describe('UsersComponent', () => { declarations: [UsersComponent, HasAnyAuthorityDirective, LocalizePipe], imports: [ ReactiveFormsModule, + RouterTestingModule, RouterModule.forChild([{ path: 'users', component: UsersComponent }]), FormsModule, ],