19 Commits

Author SHA1 Message Date
snyk-bot
d8c3e7ddf4 fix: upgrade luxon from 3.1.0 to 3.3.0
Snyk has created this PR to upgrade luxon from 3.1.0 to 3.3.0.

See this package in npm:
https://www.npmjs.com/package/luxon

See this project in Snyk:
https://app.snyk.io/org/mastermindzh/project/1579c544-8846-4f3a-a47d-08a9afd505f2?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-03-28 03:24:03 +00:00
619acfad91 Merge pull request #4 from Mastermindzh/snyk-fix-15ceb24d288dc99aa169f1fe38d9993a
[Snyk] Security upgrade nginx from 1.17.3 to 1.22.1
2023-03-27 09:56:06 +02:00
snyk-bot
5ce30c3f7e fix: Dockerfile to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-DEBIAN10-APT-1049974
- https://snyk.io/vuln/SNYK-DEBIAN10-DPKG-2847944
- https://snyk.io/vuln/SNYK-DEBIAN10-FREETYPE-1019582
- https://snyk.io/vuln/SNYK-DEBIAN10-GZIP-2444259
- https://snyk.io/vuln/SNYK-DEBIAN10-LIBTASN16-3061094
2023-03-27 07:55:50 +00:00
d16e0f2726 chore: update dependencies 2022-11-08 21:35:48 +01:00
9bb18afa14 ignore oidc fetch stuff 2022-08-30 13:47:13 +02:00
f76f91e667 Added login command for cypress and SSO protected pages 2022-08-25 14:10:46 +02:00
e20fea679a Added CypressStrictMode 2022-08-15 11:51:40 +02:00
c0a0ea66a6 eslint now receives the glob itself 2022-08-08 14:48:59 +02:00
4b61b4a370 Added styled components
- also removed existing css
  - left the capabilities included
2022-08-08 14:08:16 +02:00
e733c4a9f6 Merge pull request #2 from Inforitnl/master
Sync back to private stuff
2022-07-26 11:18:17 +02:00
b9d3025163 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)
2022-07-26 11:15:36 +02:00
8496f5cfbe Moved examples into example directory
Moved routes to separate file
Used route constants
2022-07-26 10:32:11 +02:00
de1484e9a1 Merge branch 'Mastermindzh:master' into master 2022-07-25 12:44:35 +02:00
2728820b71 Merge branch 'Mastermindzh:master' into master 2022-07-25 11:22:28 +02:00
ca0f973d07 Merge branch 'Mastermindzh:master' into master 2022-07-25 11:20:50 +02:00
126c80c7c4 Merge pull request #2 from Inforitnl/update/from-upstream
Update/from upstream
2022-07-25 11:16:50 +02:00
3d5e41ec42 Merge branch 'master' of github.com:Mastermindzh/react-starter-kit into update/from-upstream 2022-07-25 11:15:48 +02:00
dc81451685 Merge branch 'Mastermindzh:master' into master 2022-07-19 12:22:54 +02:00
d41e9d3af4 forking outside of github instructions 2022-07-19 11:17:38 +02:00
51 changed files with 6309 additions and 6856 deletions

4
.gitignore vendored
View File

@@ -70,3 +70,7 @@ bundle.zip
cypress/videos cypress/videos
cypress/screenshots cypress/screenshots
dist-tests/** dist-tests/**
# ignore oidc fetch files
public/OidcServiceWorker.js
public/OidcTrustedDomains.js

View File

@@ -2,5 +2,6 @@
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
npm run lint-staged npm run lint-staged
npm run lint
npm run test-ci npm run test-ci
npm run e2e-ci npm run e2e-ci

View File

@@ -5,13 +5,16 @@
"deepmerge", "deepmerge",
"flexbugs", "flexbugs",
"Immer", "Immer",
"Keycloak",
"languagedetector", "languagedetector",
"luxon", "luxon",
"oidc",
"pmmmwh", "pmmmwh",
"preinstall", "preinstall",
"reduxjs", "reduxjs",
"SVGR", "SVGR",
"tailwindcss", "tailwindcss",
"tendersguru",
"testid", "testid",
"typeahead", "typeahead",
"uncompiled" "uncompiled"

View File

@@ -4,7 +4,7 @@
"body": [ "body": [
"import { FunctionComponent } from \"react\";", "import { FunctionComponent } from \"react\";",
"", "",
"type Props = {}", "type Props = {};",
"", "",
"export const ${1:${TM_FILENAME_BASE}}: FunctionComponent<Props> = () => {", "export const ${1:${TM_FILENAME_BASE}}: FunctionComponent<Props> = () => {",
" return <h1>${1:${TM_FILENAME_BASE}}</h1>;", " return <h1>${1:${TM_FILENAME_BASE}}</h1>;",
@@ -68,5 +68,27 @@
"", "",
"export default ${1:${TM_FILENAME_BASE}}Slice.reducer;" "export default ${1:${TM_FILENAME_BASE}}Slice.reducer;"
] ]
},
"react-i18next useTranslate hook": {
"prefix": ["useTranslation", "translate", "i18-trans"],
"body": ["const [translate] = useTranslation();"]
},
"react-styled-component": {
"prefix": ["rsfc", "rsc", "react-styled-component"],
"body": [
"import { FunctionComponent } from \"react\";",
"import styled from \"styled-components\";",
"",
"type Props = { className?: string };",
"",
"const ${1:${TM_FILENAME_BASE}}: FunctionComponent<Props> = ({className}) => {",
" return <h1 className={className}>${1:${TM_FILENAME_BASE}}</h1>;",
"};",
"",
"const Styled${1:${TM_FILENAME_BASE}} = styled(${1:${TM_FILENAME_BASE}})``;",
"",
"export { Styled${1:${TM_FILENAME_BASE}} as ${1:${TM_FILENAME_BASE}} };",
""
]
} }
} }

View File

@@ -4,6 +4,36 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.7.0] - 2022-08-25
- Added login command for cypress and SSO protected pages
## [0.6.2] - 2022-08-15
- Added CypressStrictMode
## [0.6.1] - 2022-08-08
- eslint now receives the glob itself
## [0.6.0] - 2022-08-08
- Added styled components
- also removed existing css
- left the capabilities included
## [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
- Moved routes to separate file
- Used route constants
## [0.4.0] - 2022-07-25 ## [0.4.0] - 2022-07-25
- Added the possibility to override partial configs during deployments - Added the possibility to override partial configs during deployments

View File

@@ -1,4 +1,4 @@
FROM nginx:1.17.3 FROM nginx:1.22.1
RUN mkdir -p /usr/share/nginx/html RUN mkdir -p /usr/share/nginx/html
COPY dist/ /usr/share/nginx/html COPY dist/ /usr/share/nginx/html

View File

@@ -17,6 +17,7 @@ Includes:
- [Getting started](#getting-started) - [Getting started](#getting-started)
- [Project structure](#project-structure) - [Project structure](#project-structure)
- ["Forking" outside of Github](#forking-outside-of-github)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Using the `config.ts` file](#using-the-configts-file) - [Using the `config.ts` file](#using-the-configts-file)
- [adding values](#adding-values) - [adding values](#adding-values)
@@ -56,6 +57,17 @@ Only the important files are shown
└── tsconfig.json └── tsconfig.json
``` ```
## "Forking" outside of Github
To use this base in other git software (not Github) you will have to manually manage the upstream.
Go into your existing repo and execute the following commands:
1. `git remote add upstream <clone-url>`
2. `git pull upstream master` # or other branchname
3. `git push`
Then, when you need to sync again you can repeat step 2 and 3
## Configuration ## Configuration
This starter kit comes with runtime configuration out-of-the-box. This starter kit comes with runtime configuration out-of-the-box.

View File

@@ -13,4 +13,13 @@ export default defineConfig({
mochaFile: "dist-tests/test-results/cypress/[hash].xml", mochaFile: "dist-tests/test-results/cypress/[hash].xml",
outputs: true, outputs: true,
}, },
env: {
oidcUrl: "https://sso.mastermindzh.tech/realms/public-tests/protocol/openid-connect/token",
oidcClientId: "demo",
oidcClientSecret: "lhlPHFUd3fC1Ky0Uwyb2ssC0XiAFeGGF",
oidcGrantType: "client_credentials",
oidcScope: "openid profile email",
oidcToken: "",
oidcCallbackUrl: "http://localhost:3000/authentication/callback",
},
}); });

13
cypress/e2e/tenders.cy.ts Normal file
View File

@@ -0,0 +1,13 @@
describe("Tenders page", () => {
beforeEach(() => {
cy.oidcLogin();
// you can check that the user is logged in on this page:
cy.visit("http://localhost:3000");
});
it("Should navigate to tenders when clicking on Tenders", () => {
cy.get('[data-testid="nav.tenders"]').click();
cy.contains("tenders");
cy.contains("page_count");
});
});

View File

@@ -0,0 +1,41 @@
/// <reference types="cypress" />
/* eslint-disable camelcase */
import jwt_decode from "jwt-decode";
import "./../index";
Cypress.Commands.add("oidcLogin", () => {
const options = {
method: "POST",
url: Cypress.env("oidcUrl"),
form: true,
body: {
grant_type: Cypress.env("oidcGrantType"),
client_id: Cypress.env("oidcClientId"),
client_secret: Cypress.env("oidcClientSecret"),
scope: Cypress.env("oidcScope"),
},
};
return cy.request(options).then((response) => {
const { access_token, expires_in, id_token, token_type, scope } = response.body;
const accessTokenPayload = jwt_decode(access_token);
// stub email on the result, as service accounts don't generally have them but we use it in the UI
(accessTokenPayload as any).email = "cypress@e2e.email";
window.sessionStorage.setItem(
`oidc.default:${Cypress.env("oidcCallbackUrl")}`,
JSON.stringify({
tokens: {
accessToken: access_token,
expiresIn: expires_in,
idToken: id_token,
tokenType: token_type,
idTokenPayload: jwt_decode(id_token),
accessTokenPayload,
scope,
},
}),
);
return response;
});
});

View File

@@ -1,37 +0,0 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

View File

@@ -14,7 +14,5 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands' import "./index";
import "./auth/commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')

14
cypress/support/index.ts Normal file
View File

@@ -0,0 +1,14 @@
/* eslint-disable no-unused-vars */
// load type definitions that come with Cypress module
/// <reference types="cypress" />
export {};
declare global {
namespace Cypress {
interface Chainable {
/**
* Login to the oidc provider
*/
oidcLogin(): Chainable<Response<any>>;
}
}
}

12372
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "react-starter-kit", "name": "react-starter-kit",
"version": "0.4.0", "version": "0.7.0",
"description": "A modern, create-react-app-based, starter kit for React projects", "description": "A modern, create-react-app-based, starter kit for React projects",
"keywords": [ "keywords": [
"react", "react",
@@ -30,14 +30,14 @@
"e2e": "cypress open -d --e2e", "e2e": "cypress open -d --e2e",
"e2e-ci": "start-server-and-test start http://localhost:3000 cypress-run", "e2e-ci": "start-server-and-test start http://localhost:3000 cypress-run",
"postinstall": "husky install", "postinstall": "husky install",
"lint": "GLOBIGNORE='src/types' && eslint src/**", "lint": "eslint \"src/**\"",
"lint-staged": "lint-staged --relative", "lint-staged": "lint-staged --relative",
"organize-package-json": "npx format-package -w && npx sort-package-json", "organize-package-json": "npx format-package -w && npx sort-package-json",
"pretty-quick": "pretty-quick --staged", "pretty-quick": "pretty-quick --staged",
"start": "node scripts/start.js", "start": "node scripts/start.js",
"test": "node scripts/test.js --verbose", "test": "node scripts/test.js --verbose",
"test-ci": "node scripts/test.js --ci --coverage", "test-ci": "node scripts/test.js --ci --coverage",
"test-live-coverage": " concurrently --kill-others \"npm run test-with-coverage\" \"npx http-server -c-1 coverage/lcov-report\"", "test-live-coverage": "concurrently --kill-others \"npm run test-with-coverage\" \"npx http-server -c-1 coverage/lcov-report\"",
"test-with-coverage": "node scripts/test.js --coverage", "test-with-coverage": "node scripts/test.js --coverage",
"test:prod": "npm run test-ci && npm run e2e-ci" "test:prod": "npm run test-ci && npm run e2e-ci"
}, },
@@ -45,92 +45,97 @@
"*.{ts,js,jsx,tsx,css,scss,json,md}": "prettier --write" "*.{ts,js,jsx,tsx,css,scss,json,md}": "prettier --write"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^1.8.3", "@axa-fr/react-oidc": "^6.10.8",
"@reduxjs/toolkit": "^1.9.0",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"i18next-http-backend": "^1.4.1", "i18next-http-backend": "^2.0.1",
"luxon": "^2.4.0", "luxon": "^3.3.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-app-polyfill": "^3.0.0", "react-app-polyfill": "^3.0.0",
"react-dev-utils": "^12.0.1", "react-dev-utils": "^12.0.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-redux": "^8.0.2", "react-redux": "^8.0.5",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.4.3",
"tailwindcss": "^3.1.6" "styled-components": "^5.3.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.9", "@babel/core": "^7.20.2",
"@mastermindzh/prettier-config": "^1.0.0", "@mastermindzh/prettier-config": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.8",
"@svgr/webpack": "^5.5.0", "@svgr/webpack": "^6.5.1",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.3.0", "@testing-library/user-event": "^14.4.3",
"@types/jest": "^28.1.6", "@types/jest": "^29.2.2",
"@types/luxon": "^3.0.0", "@types/jwt-decode": "^3.1.0",
"@types/node": "^18.6.1", "@types/luxon": "^3.1.0",
"@types/react": "^18.0.15", "@types/node": "^18.11.9",
"@types/react-dom": "^18.0.6", "@types/react": "^18.0.25",
"babel-jest": "^27.4.2", "@types/react-dom": "^18.0.8",
"babel-loader": "^8.2.5", "@types/styled-components": "^5.1.26",
"babel-jest": "^29.3.0",
"babel-loader": "^9.1.0",
"babel-plugin-named-asset-import": "^0.3.8", "babel-plugin-named-asset-import": "^0.3.8",
"babel-preset-react-app": "^10.0.1", "babel-preset-react-app": "^10.0.1",
"bfj": "^7.0.2", "bfj": "^7.0.2",
"browserslist": "^4.21.2", "browserslist": "^4.21.4",
"camelcase": "^6.2.1", "camelcase": "^6.2.1",
"case-sensitive-paths-webpack-plugin": "^2.4.0", "case-sensitive-paths-webpack-plugin": "^2.4.0",
"concurrently": "^7.3.0", "concurrently": "^7.5.0",
"css-loader": "^6.7.1", "css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.2.0", "css-minimizer-webpack-plugin": "^4.2.2",
"cypress": "^10.3.1", "cypress": "^10.11.0",
"dotenv": "^10.0.0", "dotenv": "^16.0.3",
"dotenv-expand": "^5.1.0", "dotenv-expand": "^9.0.0",
"eslint": "^8.20.0", "eslint": "^8.27.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.12.1", "eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-react": "^7.30.1", "eslint-plugin-react": "^7.31.10",
"eslint-watch": "^8.0.0", "eslint-watch": "^8.0.0",
"eslint-webpack-plugin": "^3.2.0", "eslint-webpack-plugin": "^3.2.0",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"husky": "^8.0.1", "http-proxy-middleware": "^2.0.6",
"i18next": "^21.8.14", "husky": "^8.0.2",
"i18next-browser-languagedetector": "^6.1.4", "i18next": "^22.0.4",
"i18next-browser-languagedetector": "^7.0.1",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"immer": "^9.0.15", "immer": "^9.0.16",
"jest": "^27.4.3", "jest": "^29.3.0",
"jest-environment-jsdom": "^27.4.3", "jest-environment-jsdom": "^29.3.0",
"jest-junit": "^14.0.0", "jest-junit": "^14.0.1",
"jest-resolve": "^27.4.2", "jest-resolve": "^29.3.0",
"jest-watch-typeahead": "^1.0.0", "jest-watch-typeahead": "^2.2.0",
"jwt-decode": "^3.1.2",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"mini-css-extract-plugin": "^2.6.1", "mini-css-extract-plugin": "^2.6.1",
"mocha-junit-reporter": "^2.0.2", "mocha-junit-reporter": "^2.1.1",
"postcss": "^8.4.14", "postcss": "^8.4.18",
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^6.2.1", "postcss-loader": "^7.0.1",
"postcss-normalize": "^10.0.1", "postcss-normalize": "^10.0.1",
"postcss-preset-env": "^7.7.2", "postcss-preset-env": "^7.8.2",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"react-i18next": "^11.18.1", "react-i18next": "^12.0.0",
"react-refresh": "^0.14.0", "react-refresh": "^0.14.0",
"resolve": "^1.22.1", "resolve": "^1.22.1",
"resolve-url-loader": "^4.0.0", "resolve-url-loader": "^5.0.0",
"sass-loader": "^12.3.0", "sass-loader": "^13.1.0",
"semver": "^7.3.7", "semver": "^7.3.8",
"source-map-loader": "^3.0.0", "source-map-loader": "^4.0.1",
"start-server-and-test": "^1.14.0", "start-server-and-test": "^1.14.0",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.3.3", "terser-webpack-plugin": "^5.3.6",
"typescript": "^4.7.4", "typescript": "^4.8.4",
"web-vitals": "^2.1.4", "web-vitals": "^3.0.4",
"webpack": "^5.73.0", "webpack": "^5.74.0",
"webpack-dev-server": "^4.9.3", "webpack-dev-server": "^4.11.1",
"webpack-manifest-plugin": "^4.0.2", "webpack-manifest-plugin": "^5.0.0",
"workbox-webpack-plugin": "^6.5.3" "workbox-webpack-plugin": "^6.5.4"
} }
} }

View File

@@ -1,6 +1,24 @@
const defaultConfig = { const defaultConfig = {
version: "0.1.0", version: "0.1.0",
name: "React-starter-kit", 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 :) // ignore this :)

View File

@@ -10,7 +10,8 @@
"nav": { "nav": {
"home": "home", "home": "home",
"about": "about", "about": "about",
"counter": "counter" "counter": "counter",
"tenders": "tenders (with auth)"
}, },
"about": { "about": {
"title": "About" "title": "About"

View File

@@ -10,7 +10,8 @@
"nav": { "nav": {
"home": "home", "home": "home",
"about": "over ons", "about": "over ons",
"counter": "teller" "counter": "teller",
"tenders": "aanbestedingen (met auth)"
}, },
"about": { "about": {
"title": "Over ons" "title": "Over ons"

View File

@@ -1,26 +1,13 @@
import { BrowserRouter, Route, Routes } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { AboutContainer } from "./features/about/About";
import { CounterContainer } from "./features/counter/Counter";
import { HomeContainer } from "./features/home/Home";
import { Navbar } from "./infrastructure/navbar/Navbar"; import { Navbar } from "./infrastructure/navbar/Navbar";
import { AppRoutes } from "./Routes";
function App() { function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<div className="App"> <div className="App">
<Navbar /> <Navbar />
<Routes> <AppRoutes />
<Route path="/" element={<HomeContainer />} />
<Route path="/about" element={<AboutContainer />} />
<Route path="/counter" element={<CounterContainer />} />
{/* <Route index element={<Home />} /> */}
{/* <Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route> */}
{/* </Route> */}
</Routes>
</div> </div>
</BrowserRouter> </BrowserRouter>
); );

42
src/Routes.tsx Normal file
View File

@@ -0,0 +1,42 @@
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 = {};
export const ROUTE_KEYS = {
home: "",
about: "about",
counter: "counter",
tenders: "tenders",
};
export const AppRoutes: FunctionComponent<Props> = () => {
const { home, about, counter, tenders } = ROUTE_KEYS;
return (
<Routes>
<Route path={home} element={<HomeContainer />} />
<Route path={about} element={<AboutContainer />} />
<Route path={counter} element={<CounterContainer />} />
{/* a route with authentication */}
<Route
path={tenders}
element={
<OidcSecure>
<Tenders />
</OidcSecure>
}
/>
{/* <Route index element={<Home />} /> */}
{/* <Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route> */}
{/* </Route> */}
</Routes>
);
};

View File

@@ -1,5 +1,5 @@
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"; import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/state/counterSlice"; import counterReducer from "../features/examples/counter/state/counterSlice";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {

View File

@@ -2,7 +2,14 @@ import { FunctionComponent, ReactNode } from "react";
import { I18nextProvider, useTranslation } from "react-i18next"; import { I18nextProvider, useTranslation } from "react-i18next";
import i18n from "./i18n"; import i18n from "./i18n";
type Props = { children?: ReactNode; keysOnly?: boolean }; type Props = {
children?: ReactNode;
/**
* Whether to show the translation keys instead of translated text
* Can be useful to test languages that don't have full translations
*/
keysOnly?: boolean;
};
const ProvidedComponent: FunctionComponent<Props> = ({ children, keysOnly }) => { const ProvidedComponent: FunctionComponent<Props> = ({ children, keysOnly }) => {
const [_translate, i18nSettings] = useTranslation(); const [_translate, i18nSettings] = useTranslation();

View File

@@ -1,4 +0,0 @@
// A mock function to mimic making an async request for data
export function fetchCount(amount = 1) {
return new Promise<{ data: number }>((resolve) => setTimeout(() => resolve({ data: amount }), 500));
}

View File

@@ -18,7 +18,7 @@
padding-left: 16px; padding-left: 16px;
padding-right: 16px; padding-right: 16px;
margin-top: 2px; margin-top: 2px;
font-family: 'Courier New', Courier, monospace; font-family: "Courier New", Courier, monospace;
} }
.button { .button {
@@ -60,7 +60,7 @@
} }
.asyncButton:after { .asyncButton:after {
content: ''; content: "";
background-color: rgba(112, 76, 182, 0.15); background-color: rgba(112, 76, 182, 0.15);
display: block; display: block;
position: absolute; position: absolute;

View File

@@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { useAppDispatch, useAppSelector } from "../../../app/hooks";
import styles from "./Counter.module.css"; import styles from "./Counter.module.css";
import { incrementAsync } from "./state/actions/incrementAsync"; import { incrementAsync } from "./state/actions/incrementAsync";
import { import {

View File

@@ -0,0 +1,6 @@
// A mock function to mimic making an async request for data
export function fetchCount(amount = 1) {
return new Promise<{ data: number }>((resolve) =>
setTimeout(() => resolve({ data: amount }), 500),
);
}

View File

@@ -1,8 +1,4 @@
import counterReducer, { import counterReducer, { increment, decrement, incrementByAmount } from "./counterSlice";
increment,
decrement,
incrementByAmount,
} from "./counterSlice";
import { CounterState } from "../models/CounterState"; import { CounterState } from "../models/CounterState";
describe("counter reducer", () => { describe("counter reducer", () => {

View File

@@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AppThunk, RootState } from "../../../app/store"; import { AppThunk, RootState } from "../../../../app/store";
import { CounterState } from "../models/CounterState"; import { CounterState } from "../models/CounterState";
import { incrementAsync } from "./actions/incrementAsync"; import { incrementAsync } from "./actions/incrementAsync";

View File

@@ -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<Props> = () => {
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 (
<>
<h1>{translate("nav.tenders")}</h1>
{tenders ? <pre>{JSON.stringify(tenders, null, 2)}</pre> : <h2>loading...</h2>}
</>
);
};

View File

@@ -8,19 +8,39 @@ export const HomeContainer: FunctionComponent<Props> = () => {
<p>This is the react base app :)</p> <p>This is the react base app :)</p>
<span> <span>
<span>Learn </span> <span>Learn </span>
<a className="App-link" href="https://reactjs.org/" target="_blank" rel="noopener noreferrer"> <a
className="App-link"
href="https://reactjs.org/"
target="_blank"
rel="noopener noreferrer"
>
React React
</a> </a>
<span>, </span> <span>, </span>
<a className="App-link" href="https://redux.js.org/" target="_blank" rel="noopener noreferrer"> <a
className="App-link"
href="https://redux.js.org/"
target="_blank"
rel="noopener noreferrer"
>
Redux Redux
</a> </a>
<span>, </span> <span>, </span>
<a className="App-link" href="https://redux-toolkit.js.org/" target="_blank" rel="noopener noreferrer"> <a
className="App-link"
href="https://redux-toolkit.js.org/"
target="_blank"
rel="noopener noreferrer"
>
Redux Toolkit Redux Toolkit
</a> </a>
,<span> and </span> ,<span> and </span>
<a className="App-link" href="https://react-redux.js.org/" target="_blank" rel="noopener noreferrer"> <a
className="App-link"
href="https://react-redux.js.org/"
target="_blank"
rel="noopener noreferrer"
>
React Redux React Redux
</a> </a>
</span> </span>

View File

@@ -1,11 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}

View File

@@ -1,24 +1,42 @@
import React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { createGlobalStyle } from "styled-components";
import App from "./App"; import App from "./App";
import { store } from "./app/store"; import { store } from "./app/store";
import "./index.css"; import { CypressStrictMode } from "./infrastructure/CypressStrictMode";
import "./infrastructure/i18n/init"; import "./infrastructure/i18n/init";
import { OidcProvider } from "./infrastructure/sso/OidcProvider";
import { Loader } from "./infrastructure/wrappers/WithPageSuspense"; import { Loader } from "./infrastructure/wrappers/WithPageSuspense";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
const container = document.getElementById("root")!; const container = document.getElementById("root")!;
const root = createRoot(container); const root = createRoot(container);
const GlobalStyle = createGlobalStyle`
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}
`;
root.render( root.render(
<React.StrictMode> <CypressStrictMode>
<GlobalStyle />
<Provider store={store}> <Provider store={store}>
<Loader> <OidcProvider>
<App /> <Loader>
</Loader> <App />
</Loader>
</OidcProvider>
</Provider> </Provider>
</React.StrictMode>, </CypressStrictMode>,
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

View File

@@ -0,0 +1,16 @@
import React, { FunctionComponent, ReactNode } from "react";
type Props = { children?: ReactNode };
/**
* React StrictMode that disables itself when detected to be running in Cypress
*/
export const CypressStrictMode: FunctionComponent<Props> = ({ children }) => {
const isInCypress = (window as any).Cypress;
if (isInCypress) {
return <>{children}</>;
} else {
return <React.StrictMode>{children}</React.StrictMode>;
}
};

View File

@@ -1,4 +1,11 @@
import { OIDCConfig } from "../sso/models/OIDCConfig";
export interface RunTimeConfig { export interface RunTimeConfig {
version: number; version: number;
name: string; name: string;
/**
* Settings for the OIDC connection
*/
oidc: OIDCConfig;
} }

View File

@@ -1,4 +1,5 @@
import deepmerge from "deepmerge"; import deepmerge from "deepmerge";
import { RunTimeConfig } from "./RunTimeConfig";
/** /**
* gets and merges both the regular config and the override config from the window * gets and merges both the regular config and the override config from the window
@@ -12,4 +13,4 @@ export const mergeConfigs = () => {
mergeConfigs(); mergeConfigs();
export const Config = window.mergedConfig; export const Config: RunTimeConfig = window.mergedConfig;

View File

@@ -1,3 +0,0 @@
nav a {
padding-right: 10px;
}

View File

@@ -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 { WithTestTranslations } from "../../app/tests/mocks/i18n/WithTestTranslations";
import { OidcProvider } from "../sso/OidcProvider";
import { WithRouter } from "../wrappers/WithRouter"; import { WithRouter } from "../wrappers/WithRouter";
import { Navbar } from "./Navbar"; import { Navbar } from "./Navbar";
describe("Navbar container", () => { 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( render(
<WithTestTranslations> <WithTestTranslations>
<WithRouter> {/* for simple tests where we don't need auth we don't actually have to mock responses */}
<Navbar /> <OidcProvider>
</WithRouter> <WithRouter>
<Navbar />
</WithRouter>
</OidcProvider>
</WithTestTranslations>, </WithTestTranslations>,
); );
// because of the extra loaders we wait for a result just to be sure
expect(screen.getAllByTestId("nav")?.length).toBeGreaterThan(0); // see: https://testing-library.com/docs/guide-disappearance/
await waitFor(() => {
expect(screen.getAllByTestId("nav")?.length).toBeGreaterThan(0);
});
}); });
}); });

View File

@@ -1,38 +1,61 @@
import { useOidc, useOidcAccessToken } from "@axa-fr/react-oidc";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styled from "styled-components";
import { ROUTE_KEYS } from "../../Routes";
import { Config } from "../config"; import { Config } from "../config";
import "./Navbar.css"; type Props = { className?: string };
type Props = {};
export const Navbar: FunctionComponent<Props> = () => { const Navbar: FunctionComponent<Props> = ({ className }) => {
const [translate, i18n] = useTranslation(); const [translate, i18n] = useTranslation();
const { login, logout, isAuthenticated } = useOidc();
const { accessTokenPayload } = useOidcAccessToken();
const { home, about, counter } = ROUTE_KEYS;
return ( return (
<> <div className={className}>
<h1>{translate("navBar.intro")}</h1> <h1>{translate("navBar.intro")}</h1>
<p> <p>
{/* trans can also be used to translate */} {/* trans can also be used to translate */}
{Config.name} <Trans i18nKey="navBar.version">version:</Trans> {Config.name} <Trans i18nKey="navBar.version">version:</Trans>
{JSON.stringify(Config.version)} {JSON.stringify(Config.version)}
<button
onClick={() => {
isAuthenticated ? logout() : login("/");
}}
>
{isAuthenticated ? `logout (${accessTokenPayload.email})` : "login"}
</button>
</p> </p>
{/* This translation uses a formatter in the translation files */} {/* This translation uses a formatter in the translation files */}
<p>{translate("navBar.currentDate", { date: DateTime.now().toJSDate() })}</p> <p>{translate("navBar.currentDate", { date: DateTime.now().toJSDate() })}</p>
<nav data-testid="nav"> <nav data-testid="nav">
<Link to="/" data-testid="nav.home"> <Link to={home} data-testid="nav.home">
{translate("nav.home")} {translate("nav.home")}
</Link> </Link>
<Link to="/about" data-testid="nav.about"> <Link to={about} data-testid="nav.about">
{translate("nav.about")} {translate("nav.about")}
</Link> </Link>
<Link to="/counter" data-testid="nav.counter"> <Link to={counter} data-testid="nav.counter">
{translate("nav.counter")} {translate("nav.counter")}
</Link> </Link>
<Link to="/tenders" data-testid="nav.tenders">
{translate("nav.tenders")}
</Link>
<button onClick={() => i18n.changeLanguage("en")}>en</button> <button onClick={() => i18n.changeLanguage("en")}>en</button>
<button onClick={() => i18n.changeLanguage("nl")}>nl</button> <button onClick={() => i18n.changeLanguage("nl")}>nl</button>
<hr /> <hr />
</nav> </nav>
</> </div>
); );
}; };
const StyledNavBar = styled(Navbar)`
nav a {
padding-right: 10px;
}
`;
export { StyledNavBar as Navbar };

View File

@@ -0,0 +1,12 @@
import { FunctionComponent } from "react";
import styled from "styled-components";
type Props = {};
const TestComponent: FunctionComponent<Props> = () => {
return <h1>TestComponent</h1>;
};
const StyledTestComponent = styled(TestComponent)``;
export { StyledTestComponent as TestComponent };

View File

@@ -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,
};

View File

@@ -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<Props> = ({ children, ...rest }) => {
return (
<ReactOidcProvider
loadingComponent={AppLoader}
authenticatingErrorComponent={AuthenticatingError}
authenticatingComponent={Authenticating}
sessionLostComponent={SessionLost}
serviceWorkerNotSupportedComponent={ServiceWorkerNotSupported}
callbackSuccessComponent={CallBackSuccess}
configuration={SSOConfiguration}
{...rest}
>
{children}
</ReactOidcProvider>
);
};

View File

@@ -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;
}

View File

@@ -0,0 +1,3 @@
export interface SSOResult {
configurationName: string;
}

View File

@@ -0,0 +1,9 @@
import { FunctionComponent } from "react";
import { SSOResult } from "../models/SSOResult";
export const Authenticating: FunctionComponent<SSOResult> = ({ configurationName }) => (
<>
<h1>Authentication in progress for {configurationName}</h1>
<p>You will be redirected to the login page.</p>
</>
);

View File

@@ -0,0 +1,9 @@
import { FunctionComponent } from "react";
import { SSOResult } from "../models/SSOResult";
export const AuthenticatingError: FunctionComponent<SSOResult> = ({ configurationName }) => (
<>
<h1>Error for {configurationName}</h1>
<p>An error occurred during authentication.</p>
</>
);

View File

@@ -0,0 +1,9 @@
import { FunctionComponent } from "react";
import { SSOResult } from "../models/SSOResult";
export const CallBackSuccess: FunctionComponent<SSOResult> = ({ configurationName }) => (
<>
<h1>Authentication complete for {configurationName}</h1>
<p>You will be redirected...</p>
</>
);

View File

@@ -0,0 +1,9 @@
import { FunctionComponent } from "react";
import { SSOResult } from "../models/SSOResult";
export const ServiceWorkerNotSupported: FunctionComponent<SSOResult> = ({ configurationName }) => (
<>
<h1>Unable to authenticate on this browser for {configurationName}</h1>
<p>Your browser is not configured to support Service Workers.</p>
</>
);

View File

@@ -0,0 +1,17 @@
import { useOidc } from "@axa-fr/react-oidc";
import { FunctionComponent } from "react";
import { SSOResult } from "../models/SSOResult";
export const SessionLost: FunctionComponent<SSOResult> = ({ configurationName }) => {
const { login } = useOidc(configurationName);
return (
<>
<h1>Session timed out for {configurationName}</h1>
<p>Your session has expired. Please re-authenticate.</p>
<button type="button" onClick={() => login("/")}>
Login
</button>
</>
);
};

28
src/setupProxy.js Normal file
View File

@@ -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");
};