From b9d302516345b5d517291f0cdf4145a854602423 Mon Sep 17 00:00:00 2001 From: Mastermindzh Date: Tue, 26 Jul 2022 11:15:36 +0200 Subject: [PATCH] Added react-oidc (use demo/demo) Added an example of an authentication protected page (tenders) Added an example with the built in proxy (to combat CORS) (tendersguru) --- .vscode/settings.json | 3 + .vscode/typescriptreact.code-snippets | 4 + CHANGELOG.md | 6 + package-lock.json | 138 +++++++++++++++--- package.json | 4 +- public/config.js | 18 +++ public/i18n/en.json | 3 +- public/i18n/nl.json | 3 +- src/Routes.tsx | 14 +- src/features/examples/tenders/Tenders.tsx | 33 +++++ src/index.tsx | 9 +- src/infrastructure/config/RunTimeConfig.ts | 7 + src/infrastructure/navbar/Navbar.spec.tsx | 21 ++- src/infrastructure/navbar/Navbar.tsx | 13 ++ src/infrastructure/sso/Configuration.ts | 14 ++ src/infrastructure/sso/OidcProvider.tsx | 28 ++++ src/infrastructure/sso/models/OIDCConfig.tsx | 31 ++++ src/infrastructure/sso/models/SSOResult.tsx | 3 + .../sso/overrides/Authenticating.tsx | 9 ++ .../sso/overrides/AuthenticatingError.tsx | 9 ++ .../sso/overrides/CallBackSuccess.tsx | 9 ++ .../overrides/ServiceWorkerNotSupported.tsx | 9 ++ .../sso/overrides/SessionLost.tsx | 17 +++ src/setupProxy.js | 28 ++++ 24 files changed, 400 insertions(+), 33 deletions(-) create mode 100644 src/features/examples/tenders/Tenders.tsx create mode 100644 src/infrastructure/sso/Configuration.ts create mode 100644 src/infrastructure/sso/OidcProvider.tsx create mode 100644 src/infrastructure/sso/models/OIDCConfig.tsx create mode 100644 src/infrastructure/sso/models/SSOResult.tsx create mode 100644 src/infrastructure/sso/overrides/Authenticating.tsx create mode 100644 src/infrastructure/sso/overrides/AuthenticatingError.tsx create mode 100644 src/infrastructure/sso/overrides/CallBackSuccess.tsx create mode 100644 src/infrastructure/sso/overrides/ServiceWorkerNotSupported.tsx create mode 100644 src/infrastructure/sso/overrides/SessionLost.tsx create mode 100644 src/setupProxy.js diff --git a/.vscode/settings.json b/.vscode/settings.json index ddb43e6..d8c88d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,13 +5,16 @@ "deepmerge", "flexbugs", "Immer", + "Keycloak", "languagedetector", "luxon", + "oidc", "pmmmwh", "preinstall", "reduxjs", "SVGR", "tailwindcss", + "tendersguru", "testid", "typeahead", "uncompiled" diff --git a/.vscode/typescriptreact.code-snippets b/.vscode/typescriptreact.code-snippets index 7a13705..a95519e 100644 --- a/.vscode/typescriptreact.code-snippets +++ b/.vscode/typescriptreact.code-snippets @@ -68,5 +68,9 @@ "", "export default ${1:${TM_FILENAME_BASE}}Slice.reducer;" ] + }, + "react-i18next useTranslate hook": { + "prefix": ["useTranslation", "translate", "i18-trans"], + "body": ["const [translate] = useTranslation();"] } } diff --git a/CHANGELOG.md b/CHANGELOG.md index b675478..5c92129 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.0] - 2022-06-26 + +- Added react-oidc (use demo/demo) +- Added an example of an authentication protected page (tenders) +- Added an example with the built in proxy (to combat CORS) (tendersguru) + ## [0.4.1] - 2022-06-26 - Moved examples into example directory diff --git a/package-lock.json b/package-lock.json index eef3d90..f1b49ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@axa-fr/react-oidc": "^6.0.0-beta10", "@reduxjs/toolkit": "^1.8.3", "deepmerge": "^4.2.2", "i18next-http-backend": "^1.4.1", @@ -60,6 +61,7 @@ "file-loader": "^6.2.0", "fs-extra": "^10.1.0", "html-webpack-plugin": "^5.5.0", + "http-proxy-middleware": "^2.0.6", "husky": "^8.0.1", "i18next": "^21.8.14", "i18next-browser-languagedetector": "^6.1.4", @@ -112,6 +114,18 @@ "node": ">=6.0.0" } }, + "node_modules/@axa-fr/react-oidc": { + "version": "6.0.0-beta10", + "resolved": "https://registry.npmjs.org/@axa-fr/react-oidc/-/react-oidc-6.0.0-beta10.tgz", + "integrity": "sha512-BWHuIn9b+5O35It+V3m/8MWAV6pOjoMy2SihKfeDlsQs2Bp4HMBb4x46zygZ1IGs/u43jtTsnZkrPhwJU6VdqQ==", + "dependencies": { + "@openid/appauth": "1.3.1" + }, + "peerDependencies": { + "react": "x", + "react-dom": "x" + } + }, "node_modules/@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -3309,6 +3323,32 @@ "node": ">= 8" } }, + "node_modules/@openid/appauth": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@openid/appauth/-/appauth-1.3.1.tgz", + "integrity": "sha512-e54kpi219wES2ijPzeHe1kMnT8VKH8YeTd1GAn9BzVBmutz3tBgcG1y8a4pziNr4vNjFnuD4W446Ua7ELnNDiA==", + "dependencies": { + "@types/base64-js": "^1.3.0", + "@types/jquery": "^3.5.5", + "base64-js": "^1.5.1", + "follow-redirects": "^1.13.3", + "form-data": "^4.0.0", + "opener": "^1.5.2" + } + }, + "node_modules/@openid/appauth/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz", @@ -4019,6 +4059,11 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ZmI0sZGAUNXUfMWboWwi4LcfpoVUYldyN6Oe0oJ5cCsHDU/LlRq8nQKPXhYLOx36QYSW9bNIb1vvRrD6K7Llgw==" + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -4203,6 +4248,14 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/@types/jquery": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz", + "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==", + "dependencies": { + "@types/sizzle": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -4338,8 +4391,7 @@ "node_modules/@types/sizzle": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", - "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", - "dev": true + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==" }, "node_modules/@types/sockjs": { "version": "0.3.33", @@ -5252,8 +5304,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -5666,7 +5717,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -6354,7 +6404,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -7540,7 +7589,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -9636,7 +9684,6 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", - "dev": true, "funding": [ { "type": "individual", @@ -15653,6 +15700,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -21337,6 +21392,14 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@axa-fr/react-oidc": { + "version": "6.0.0-beta10", + "resolved": "https://registry.npmjs.org/@axa-fr/react-oidc/-/react-oidc-6.0.0-beta10.tgz", + "integrity": "sha512-BWHuIn9b+5O35It+V3m/8MWAV6pOjoMy2SihKfeDlsQs2Bp4HMBb4x46zygZ1IGs/u43jtTsnZkrPhwJU6VdqQ==", + "requires": { + "@openid/appauth": "1.3.1" + } + }, "@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -23588,6 +23651,31 @@ "fastq": "^1.6.0" } }, + "@openid/appauth": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@openid/appauth/-/appauth-1.3.1.tgz", + "integrity": "sha512-e54kpi219wES2ijPzeHe1kMnT8VKH8YeTd1GAn9BzVBmutz3tBgcG1y8a4pziNr4vNjFnuD4W446Ua7ELnNDiA==", + "requires": { + "@types/base64-js": "^1.3.0", + "@types/jquery": "^3.5.5", + "base64-js": "^1.5.1", + "follow-redirects": "^1.13.3", + "form-data": "^4.0.0", + "opener": "^1.5.2" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz", @@ -24067,6 +24155,11 @@ "@babel/types": "^7.3.0" } }, + "@types/base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ZmI0sZGAUNXUfMWboWwi4LcfpoVUYldyN6Oe0oJ5cCsHDU/LlRq8nQKPXhYLOx36QYSW9bNIb1vvRrD6K7Llgw==" + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -24244,6 +24337,14 @@ } } }, + "@types/jquery": { + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz", + "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==", + "requires": { + "@types/sizzle": "*" + } + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -24379,8 +24480,7 @@ "@types/sizzle": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", - "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", - "dev": true + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==" }, "@types/sockjs": { "version": "0.3.33", @@ -25058,8 +25158,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "at-least-node": { "version": "1.0.0", @@ -25369,8 +25468,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "batch": { "version": "0.6.1", @@ -25874,7 +25972,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -26742,8 +26839,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "depd": { "version": "2.0.0", @@ -28331,8 +28427,7 @@ "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", - "dev": true + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" }, "forever-agent": { "version": "0.6.1", @@ -32760,6 +32855,11 @@ "is-wsl": "^2.2.0" } }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==" + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", diff --git a/package.json b/package.json index 85d48be..bf6826d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-starter-kit", - "version": "0.4.1", + "version": "0.5.0", "description": "A modern, create-react-app-based, starter kit for React projects", "keywords": [ "react", @@ -45,6 +45,7 @@ "*.{ts,js,jsx,tsx,css,scss,json,md}": "prettier --write" }, "dependencies": { + "@axa-fr/react-oidc": "^6.0.0-beta10", "@reduxjs/toolkit": "^1.8.3", "deepmerge": "^4.2.2", "i18next-http-backend": "^1.4.1", @@ -95,6 +96,7 @@ "file-loader": "^6.2.0", "fs-extra": "^10.1.0", "html-webpack-plugin": "^5.5.0", + "http-proxy-middleware": "^2.0.6", "husky": "^8.0.1", "i18next": "^21.8.14", "i18next-browser-languagedetector": "^6.1.4", diff --git a/public/config.js b/public/config.js index ca54e78..83fc5c5 100644 --- a/public/config.js +++ b/public/config.js @@ -1,6 +1,24 @@ const defaultConfig = { version: "0.1.0", name: "React-starter-kit", + // oidc: { + // url: "private_url", + // realm: "inforit", + // clientId: "react-base", + // scope: "openid profile email", + // redirectUri: "http://localhost:3000/authentication/callback", + // silentRedirectUri: "http://localhost:3000/authentication/silent-callback", + // serviceWorkerOnly: false, + // }, + oidc: { + url: "https://sso.mastermindzh.tech/realms/public-tests", + realm: "public-tests", + clientId: "react-starter-kit", + scope: "openid profile email", + redirectUri: "http://localhost:3000/authentication/callback", + silentRedirectUri: "http://localhost:3000/authentication/silent-callback", + serviceWorkerOnly: false, + }, }; // ignore this :) diff --git a/public/i18n/en.json b/public/i18n/en.json index 05508d1..1b565a8 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -10,7 +10,8 @@ "nav": { "home": "home", "about": "about", - "counter": "counter" + "counter": "counter", + "tenders": "tenders (with auth)" }, "about": { "title": "About" diff --git a/public/i18n/nl.json b/public/i18n/nl.json index d60937c..04d2aa3 100644 --- a/public/i18n/nl.json +++ b/public/i18n/nl.json @@ -10,7 +10,8 @@ "nav": { "home": "home", "about": "over ons", - "counter": "teller" + "counter": "teller", + "tenders": "aanbestedingen (met auth)" }, "about": { "title": "Over ons" diff --git a/src/Routes.tsx b/src/Routes.tsx index 5ba45b4..fa525a7 100644 --- a/src/Routes.tsx +++ b/src/Routes.tsx @@ -1,7 +1,9 @@ +import { OidcSecure } from "@axa-fr/react-oidc"; import { FunctionComponent } from "react"; import { Route, Routes } from "react-router-dom"; import { AboutContainer } from "./features/about/About"; import { CounterContainer } from "./features/examples/counter/Counter"; +import { Tenders } from "./features/examples/tenders/Tenders"; import { HomeContainer } from "./features/home/Home"; type Props = {}; @@ -9,15 +11,25 @@ export const ROUTE_KEYS = { home: "", about: "about", counter: "counter", + tenders: "tenders", }; export const AppRoutes: FunctionComponent = () => { - const { home, about, counter } = ROUTE_KEYS; + const { home, about, counter, tenders } = ROUTE_KEYS; return ( } /> } /> } /> + {/* a route with authentication */} + + + + } + /> {/* } /> */} {/* }> } /> diff --git a/src/features/examples/tenders/Tenders.tsx b/src/features/examples/tenders/Tenders.tsx new file mode 100644 index 0000000..ec42600 --- /dev/null +++ b/src/features/examples/tenders/Tenders.tsx @@ -0,0 +1,33 @@ +import { useOidcAccessToken } from "@axa-fr/react-oidc"; +import { FunctionComponent, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +type Props = {}; + +export const Tenders: FunctionComponent = () => { + const [tenders, setTenders] = useState(null); + const [translate] = useTranslation(); + const { accessToken } = useOidcAccessToken(); + + useEffect(() => { + // this uses the CORS proxy in our webpack setup + // tendersguru is mapped to "https://tenders.guru" in setupProxy.js + // it will strip the tendersguru part, so the full url will be: https://tenders.guru/api/ro/tenders + fetch("tendersguru/api/ro/tenders", { + // fetch's way of adding headers. Not required to access the api... but :shrug: + headers: new Headers({ + Authorization: `Bearer ${accessToken}`, + }), + }) + .then((response) => response.json()) + .then((data) => { + setTenders(data); + }); + }, [accessToken]); + return ( + <> +

{translate("nav.tenders")}

+ {tenders ?
{JSON.stringify(tenders, null, 2)}
:

loading...

} + + ); +}; diff --git a/src/index.tsx b/src/index.tsx index 75f176e..70501cb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,7 @@ import App from "./App"; import { store } from "./app/store"; import "./index.css"; import "./infrastructure/i18n/init"; +import { OidcProvider } from "./infrastructure/sso/OidcProvider"; import { Loader } from "./infrastructure/wrappers/WithPageSuspense"; import reportWebVitals from "./reportWebVitals"; @@ -14,9 +15,11 @@ const root = createRoot(container); root.render( - - - + + + + + , ); diff --git a/src/infrastructure/config/RunTimeConfig.ts b/src/infrastructure/config/RunTimeConfig.ts index 9376677..5dc4212 100644 --- a/src/infrastructure/config/RunTimeConfig.ts +++ b/src/infrastructure/config/RunTimeConfig.ts @@ -1,4 +1,11 @@ +import { OIDCConfig } from "../sso/models/OIDCConfig"; + export interface RunTimeConfig { version: number; name: string; + + /** + * Settings for the OIDC connection + */ + oidc: OIDCConfig; } diff --git a/src/infrastructure/navbar/Navbar.spec.tsx b/src/infrastructure/navbar/Navbar.spec.tsx index 3685923..5dd351d 100644 --- a/src/infrastructure/navbar/Navbar.spec.tsx +++ b/src/infrastructure/navbar/Navbar.spec.tsx @@ -1,18 +1,25 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import { WithTestTranslations } from "../../app/tests/mocks/i18n/WithTestTranslations"; +import { OidcProvider } from "../sso/OidcProvider"; import { WithRouter } from "../wrappers/WithRouter"; import { Navbar } from "./Navbar"; describe("Navbar container", () => { - it.only("renders a navigation section identified by the nav test-id", () => { + it.only("renders a navigation section identified by the nav test-id", async () => { render( - - - + {/* for simple tests where we don't need auth we don't actually have to mock responses */} + + + + + , ); - - expect(screen.getAllByTestId("nav")?.length).toBeGreaterThan(0); + // because of the extra loaders we wait for a result just to be sure + // see: https://testing-library.com/docs/guide-disappearance/ + await waitFor(() => { + expect(screen.getAllByTestId("nav")?.length).toBeGreaterThan(0); + }); }); }); diff --git a/src/infrastructure/navbar/Navbar.tsx b/src/infrastructure/navbar/Navbar.tsx index dff7bd9..af3783a 100644 --- a/src/infrastructure/navbar/Navbar.tsx +++ b/src/infrastructure/navbar/Navbar.tsx @@ -1,3 +1,4 @@ +import { useOidc, useOidcAccessToken } from "@axa-fr/react-oidc"; import { DateTime } from "luxon"; import { FunctionComponent } from "react"; import { Trans, useTranslation } from "react-i18next"; @@ -9,6 +10,8 @@ type Props = {}; export const Navbar: FunctionComponent = () => { const [translate, i18n] = useTranslation(); + const { login, logout, isAuthenticated } = useOidc(); + const { accessTokenPayload } = useOidcAccessToken(); const { home, about, counter } = ROUTE_KEYS; return ( <> @@ -17,6 +20,13 @@ export const Navbar: FunctionComponent = () => { {/* trans can also be used to translate */} {Config.name} version: {JSON.stringify(Config.version)} +

{/* This translation uses a formatter in the translation files */} @@ -31,6 +41,9 @@ export const Navbar: FunctionComponent = () => { {translate("nav.counter")} + + {translate("nav.tenders")} +
diff --git a/src/infrastructure/sso/Configuration.ts b/src/infrastructure/sso/Configuration.ts new file mode 100644 index 0000000..57125a9 --- /dev/null +++ b/src/infrastructure/sso/Configuration.ts @@ -0,0 +1,14 @@ +import { OidcConfiguration } from "@axa-fr/react-oidc/dist/vanilla/oidc"; +import { Config } from "../config"; + +const { clientId, redirectUri, silentRedirectUri, scope, url } = Config.oidc; + +/* eslint-disable camelcase */ +export const SSOConfiguration: OidcConfiguration = { + client_id: clientId, + redirect_uri: redirectUri, + silent_redirect_uri: silentRedirectUri, + scope, + authority: url, + service_worker_only: false, +}; diff --git a/src/infrastructure/sso/OidcProvider.tsx b/src/infrastructure/sso/OidcProvider.tsx new file mode 100644 index 0000000..ff1fbdf --- /dev/null +++ b/src/infrastructure/sso/OidcProvider.tsx @@ -0,0 +1,28 @@ +import { OidcProvider as ReactOidcProvider } from "@axa-fr/react-oidc"; +import { FunctionComponent, ReactNode } from "react"; +import { AppLoader } from "../loader/appLoader"; +import { SSOConfiguration } from "./Configuration"; +import { Authenticating } from "./overrides/Authenticating"; +import { AuthenticatingError } from "./overrides/AuthenticatingError"; +import { CallBackSuccess } from "./overrides/CallBackSuccess"; +import { ServiceWorkerNotSupported } from "./overrides/ServiceWorkerNotSupported"; +import { SessionLost } from "./overrides/SessionLost"; + +type Props = { children?: ReactNode; [x: string]: any }; + +export const OidcProvider: FunctionComponent = ({ children, ...rest }) => { + return ( + + {children} + + ); +}; diff --git a/src/infrastructure/sso/models/OIDCConfig.tsx b/src/infrastructure/sso/models/OIDCConfig.tsx new file mode 100644 index 0000000..3e02433 --- /dev/null +++ b/src/infrastructure/sso/models/OIDCConfig.tsx @@ -0,0 +1,31 @@ +export interface OIDCConfig { + /** + * Authority URL + */ + url: string; + + /** + * Realm to authenticate against + */ + realm: string; + + /** + * Id of this client + */ + clientId: string; + + /** + * Scope(s) that you want to request + */ + scope: string; + + /** + * URI to redirect to after successful login + */ + redirectUri: string; + + /** + * redirect uri for the silent refresh + */ + silentRedirectUri: string; +} diff --git a/src/infrastructure/sso/models/SSOResult.tsx b/src/infrastructure/sso/models/SSOResult.tsx new file mode 100644 index 0000000..830f66f --- /dev/null +++ b/src/infrastructure/sso/models/SSOResult.tsx @@ -0,0 +1,3 @@ +export interface SSOResult { + configurationName: string; +} diff --git a/src/infrastructure/sso/overrides/Authenticating.tsx b/src/infrastructure/sso/overrides/Authenticating.tsx new file mode 100644 index 0000000..54bd54a --- /dev/null +++ b/src/infrastructure/sso/overrides/Authenticating.tsx @@ -0,0 +1,9 @@ +import { FunctionComponent } from "react"; +import { SSOResult } from "../models/SSOResult"; + +export const Authenticating: FunctionComponent = ({ configurationName }) => ( + <> +

Authentication in progress for {configurationName}

+

You will be redirected to the login page.

+ +); diff --git a/src/infrastructure/sso/overrides/AuthenticatingError.tsx b/src/infrastructure/sso/overrides/AuthenticatingError.tsx new file mode 100644 index 0000000..bd8f7bd --- /dev/null +++ b/src/infrastructure/sso/overrides/AuthenticatingError.tsx @@ -0,0 +1,9 @@ +import { FunctionComponent } from "react"; +import { SSOResult } from "../models/SSOResult"; + +export const AuthenticatingError: FunctionComponent = ({ configurationName }) => ( + <> +

Error for {configurationName}

+

An error occurred during authentication.

+ +); diff --git a/src/infrastructure/sso/overrides/CallBackSuccess.tsx b/src/infrastructure/sso/overrides/CallBackSuccess.tsx new file mode 100644 index 0000000..9b52df3 --- /dev/null +++ b/src/infrastructure/sso/overrides/CallBackSuccess.tsx @@ -0,0 +1,9 @@ +import { FunctionComponent } from "react"; +import { SSOResult } from "../models/SSOResult"; + +export const CallBackSuccess: FunctionComponent = ({ configurationName }) => ( + <> +

Authentication complete for {configurationName}

+

You will be redirected...

+ +); diff --git a/src/infrastructure/sso/overrides/ServiceWorkerNotSupported.tsx b/src/infrastructure/sso/overrides/ServiceWorkerNotSupported.tsx new file mode 100644 index 0000000..1bf88bb --- /dev/null +++ b/src/infrastructure/sso/overrides/ServiceWorkerNotSupported.tsx @@ -0,0 +1,9 @@ +import { FunctionComponent } from "react"; +import { SSOResult } from "../models/SSOResult"; + +export const ServiceWorkerNotSupported: FunctionComponent = ({ configurationName }) => ( + <> +

Unable to authenticate on this browser for {configurationName}

+

Your browser is not configured to support Service Workers.

+ +); diff --git a/src/infrastructure/sso/overrides/SessionLost.tsx b/src/infrastructure/sso/overrides/SessionLost.tsx new file mode 100644 index 0000000..5cb53a7 --- /dev/null +++ b/src/infrastructure/sso/overrides/SessionLost.tsx @@ -0,0 +1,17 @@ +import { useOidc } from "@axa-fr/react-oidc"; +import { FunctionComponent } from "react"; +import { SSOResult } from "../models/SSOResult"; + +export const SessionLost: FunctionComponent = ({ configurationName }) => { + const { login } = useOidc(configurationName); + + return ( + <> +

Session timed out for {configurationName}

+

Your session has expired. Please re-authenticate.

+ + + ); +}; diff --git a/src/setupProxy.js b/src/setupProxy.js new file mode 100644 index 0000000..31dafb5 --- /dev/null +++ b/src/setupProxy.js @@ -0,0 +1,28 @@ +const { createProxyMiddleware } = require("http-proxy-middleware"); + +/** + * Create a simple proxy that automatically removes the prefix endpoint + * @param {*} app app to configure proxy on + * @param {*} endpoint endpoint you want to use for the proxy + * @param {*} target proxy target + */ +const createSimpleProxy = (app, endpoint, target) => { + app.use( + endpoint, + createProxyMiddleware({ + target, + changeOrigin: true, + pathRewrite: { + [`^${endpoint}`]: "", + }, + }), + ); +}; + +/** + * Actual proxy configuration + * @param {*} app app to configure proxy on + */ +module.exports = function (app) { + createSimpleProxy(app, "/tendersguru", "https://tenders.guru"); +};