From b6f0546c6dfb9a876ea0c028ca00ea2320d61472 Mon Sep 17 00:00:00 2001 From: Lea Renaux Date: Tue, 4 Jun 2024 15:03:24 +0200 Subject: [PATCH] rework App.jsx and add configuration & oidc in index.js --- package.json | 2 +- public/configuration.json | 5 +- src/Authentication/oidc.js | 56 +- src/Authentication/useAuth.js | 61 +- src/components/App/App.jsx | 88 +- src/components/App/App.test.jsx | 69 +- src/components/App/AppHOC.jsx | 19 - .../App/__snapshots__/App.test.jsx.snap | 2386 +---------------- src/index.js | 66 +- src/utils/Service.test.js | 2 +- 10 files changed, 159 insertions(+), 2595 deletions(-) delete mode 100644 src/components/App/AppHOC.jsx diff --git a/package.json b/package.json index ebf3a97..7b9c4d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sonor", - "version": "0.5.33", + "version": "0.5.34", "private": true, "dependencies": { "@tanstack/react-query": "4.0.5", diff --git a/public/configuration.json b/public/configuration.json index 2274d68..4294295 100644 --- a/public/configuration.json +++ b/public/configuration.json @@ -2,8 +2,9 @@ "PEARL_JAM_URL": "http://localhost:7777", "QUEEN_URL_BACK_END": "http://localhost:7777", "QUEEN_URL_FRONT_END": "http://localhost:7777", - + "ISSUER_URI": "http://localhost:7777", "AUTHENTICATION_MODE": "anonymous", - "_AUTHENTICATION_MODE_COMMENT_": "Use 'keycloak' or 'anonymous'" + "OIDC_CLIENT_ID": "clientId", + "_AUTHENTICATION_MODE_COMMENT_": "Use 'oidc' or 'anonymous'" } diff --git a/src/Authentication/oidc.js b/src/Authentication/oidc.js index 0f3d030..5d2e30d 100644 --- a/src/Authentication/oidc.js +++ b/src/Authentication/oidc.js @@ -1,42 +1,42 @@ import { createMockReactOidc } from "oidc-spa/mock/react"; import { createReactOidc } from "oidc-spa/react"; +import { useEffect, useState } from "react"; const guestUser = { inseegroupedefaut: [], - preferred_username: "Guest", - name: "Guest", + preferred_username: "anonymous", + name: "anonymous", }; -// const publicUrl = new URL(process.env.PUBLIC_URL!, window.location.href); -// const response = await fetch(`${publicUrl.origin}/configuration.json`); -// const configuration = await response.json(); -// const isOidc = configuration.AUTHENTICATION_MODE === "oidc" -const isOidc = true - -const getConfiguration = async () => { - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); - const response = await fetch(`${publicUrl.origin}/configuration.json`) - const configuration = await response.json(); - console.log("configuration", configuration) - return configuration +export const useConfiguration = () => { + const [configuration, setConfiguration] = useState() + useEffect(() => { + const getConfiguration = async () => { + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + const response = await fetch(`${publicUrl.origin}/configuration.json`) + const configuration = await response.json(); + setConfiguration(configuration) + window.localStorage.setItem( + 'AUTHENTICATION_MODE', + configuration.AUTHENTICATION_MODE, + ); + window.localStorage.setItem('PEARL_JAM_URL', configuration.PEARL_JAM_URL); + window.localStorage.setItem('QUEEN_URL_BACK_END', configuration.QUEEN_URL_BACK_END); + window.localStorage.setItem('QUEEN_URL_FRONT_END', configuration.QUEEN_URL_FRONT_END); + } + getConfiguration() + }, []) + + return configuration } -export const createAppOidc = () => { - // const configuration = getConfiguration() - // console.log("dans oidc",configuration) - - if (isOidc) { +export const createAppOidc = (configuration) => { + if(configuration && configuration.AUTHENTICATION_MODE === "oidc"){ return createReactOidc({ - // issuerUri: configuration.ISSUER_URI, - // clientId: configuration.OIDC_CLIENT_ID, - - issuerUri:"https://auth.insee.test/auth/realms/agents-insee-interne", - clientId:"localhost-frontend", - - publicUrl: "/", - // extraQueryParams: { kc_idp_hint: import.meta.env.VITE_IDENTITY_PROVIDER }, - extraQueryParams: {kc_idp_hint :"insee-ssp"} + issuerUri: configuration.ISSUER_URI, + clientId: configuration.OIDC_CLIENT_ID, + publicUrl: "/", }); } diff --git a/src/Authentication/useAuth.js b/src/Authentication/useAuth.js index 22f41e0..932a3a5 100644 --- a/src/Authentication/useAuth.js +++ b/src/Authentication/useAuth.js @@ -1,29 +1,40 @@ import { useEffect } from "react"; -import { createAppOidc } from "./oidc.js"; - -export const { OidcProvider, prOidc, useOidc } = createAppOidc(); - -export const useHasRole = (role)=> { - const { oidcTokens } = useOidc({ assertUserLoggedIn: true }); - return oidcTokens.decodedIdToken.inseegroupedefaut.includes(role); -}; - -export const useAccessToken = () => { - return useOidc({ assertUserLoggedIn: true }).oidcTokens.accessToken; -}; - -export const useUser = () => { - return useOidc({ assertUserLoggedIn: true }).oidcTokens.decodedIdToken; -}; - -export const useMaybeUser = () => { - return useOidc({ assertUserLoggedIn: false }).oidcTokens?.decodedIdToken; -}; +import { createReactOidc } from "oidc-spa/react"; + +/** + * By default, without initialization we use a mock as a return of "useOidc" + * + * This object will be used for testing purpose to simulate authentication status + */ +const mockOidc = { login: () => {}, isUserLoggedIn: false, oidcTokens: {} } +// Global method that will be replaced when oidc is initialized +let useOidc = () => mockOidc; + +/** + * Helper method used for tests, set a fake Oidc authentication state + */ +export const mockOidcForUser = () => { + window.localStorage.setItem("AUTHENTICATION_MODE", "oidc") + mockOidc.isUserLoggedIn = true + mockOidc.oidcTokens = {accessToken: '12031203'} +} +export const mockOidcFailed = () => { + mockOidc.isUserLoggedIn = false + mockOidc.oidcTokens = {} +} -export const useLogout = () => { - return useOidc({ assertUserLoggedIn: false }).logout; -}; +/** + * Initialize oidc + */ +export function initializeOidc (config) { + const oidc = createReactOidc(config) + useOidc = oidc.useOidc + return oidc; +} +/** + * Retrieve authentication status based of Oidc + */ export function useIsAuthenticated() { const { login, isUserLoggedIn, oidcTokens } = useOidc({ assertUserLoggedIn: false }); @@ -32,11 +43,9 @@ export function useIsAuthenticated() { return; } login({ - doesCurrentHrefRequiresAuth: false, + doesCurrentHrefRequiresAuth: true, }); }, [login]); return { isAuthenticated: isUserLoggedIn, tokens: oidcTokens }; } - -export const AuthProvider = OidcProvider; diff --git a/src/components/App/App.jsx b/src/components/App/App.jsx index 6835c29..db9b820 100644 --- a/src/components/App/App.jsx +++ b/src/components/App/App.jsx @@ -1,81 +1,55 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { useIsAuthenticated } from "../../Authentication/useAuth"; +import D from "../../i18n"; import View from "../View/View"; import DataFormatter from "../../utils/DataFormatter"; import { OIDC, ANONYMOUS } from "../../utils/constants.json"; -import initConfiguration from "../../initConfiguration"; -import D from "../../i18n"; -class App extends React.Component { - constructor(props) { - super(props); - this.state = { - authenticated: false, - contactFailed: false, - initialisationFailed: false, - data: null, - }; - } +export const App = () => { + const [authenticated, setAuthenticated] = useState(false); + const [contactFailed, setContactFailed] = useState(false); + const [data, setData] = useState(null); - async componentDidMount() { - try { - await initConfiguration(); - } catch (e) { - this.setState({ initialisationFailed: true }); - } + const { tokens } = useIsAuthenticated(); + + useEffect(() => { if (window.localStorage.getItem("AUTHENTICATION_MODE") === ANONYMOUS) { const dataRetreiver = new DataFormatter(); dataRetreiver.getUserInfo((data) => { if (data.error) { - this.setState({ contactFailed: true }); + setContactFailed(true); } else { - this.setState({ authenticated: true, data }); + setAuthenticated(true); + setData(data); } }); } else if ( window.localStorage.getItem("AUTHENTICATION_MODE") === OIDC && - this.props.token + tokens?.accessToken ) { - const dataRetreiver = new DataFormatter(this.props.token); + const dataRetreiver = new DataFormatter(tokens.accessToken); dataRetreiver.getUserInfo((data) => { - this.setState({ authenticated: data !== undefined, data }); + setAuthenticated(data !== undefined); + setData(data); }); } - } + }, [tokens]); - async componentDidUpdate(prevprops, prevstate) { - const { token } = this.props; - - if ( - (token && token !== prevprops.token) || - this.state.authenticated !== prevstate.authenticated - ) { - const dataRetreiver = new DataFormatter(this.props.token); - dataRetreiver.getUserInfo((data) => { - this.setState({ authenticated: data !== undefined, data }); - }); - } + if (!tokens?.accessToken) { + return
{D.initializationFailed}
; } - render() { - const { authenticated, data, contactFailed, initialisationFailed } = - this.state; - - if (authenticated) { - return ( -
- -
- ); - } - if (initialisationFailed) { - return
{D.initializationFailed}
; - } - if (contactFailed) { - return
{D.cannotContactServer}
; - } + if (authenticated && tokens?.accessToken && data) { + return ( +
+ +
+ ); + } - return
{D.initializing}
; + if (contactFailed) { + return
{D.cannotContactServer}
; } -} -export default App; + return
{D.initializing}
; +}; diff --git a/src/components/App/App.test.jsx b/src/components/App/App.test.jsx index 1ada40c..93a2c5a 100644 --- a/src/components/App/App.test.jsx +++ b/src/components/App/App.test.jsx @@ -1,14 +1,12 @@ // Link.react.test.js import React from 'react'; -import { - render, screen, cleanup, waitForElement, -} from '@testing-library/react'; -import Keycloak from 'keycloak-js'; +import { render, screen, cleanup } from "@testing-library/react"; import { NotificationManager } from 'react-notifications'; import DataFormatter from '../../utils/DataFormatter'; -import App from './App'; +import { App } from "./App"; import mocks from '../../tests/mocks'; import C from '../../utils/constants.json'; +import { mockOidcForUser, mockOidcFailed } from "../../Authentication/useAuth"; jest.mock( "../../../package.json", @@ -29,7 +27,6 @@ Date.now = jest.fn(() => 1597916474000); afterEach(cleanup); -jest.mock('keycloak-js'); jest.mock('react-notifications'); jest.mock('../../utils/DataFormatter'); jest.mock('../../initConfiguration'); @@ -53,15 +50,6 @@ const mockError = jest.fn(); NotificationManager.success = mockSuccess; NotificationManager.error = mockError; -Keycloak.init = jest.fn(() => (Promise.resolve({ token: 'abc' }))); - -Keycloak.mockImplementation(() => ({ - init: jest.fn(() => (Promise.resolve({ token: 'abc' }))), - updateToken: (() => ({ error: (() => {}) })), - tokenParsed: { exp: 300 }, - timeSkew: 0, -})); - const updatePreferences = jest.fn((newPrefs, cb) => { if (newPrefs.includes('simpsonkgs2020x00')) { cb({ status: 500 }); @@ -104,52 +92,15 @@ DataFormatter.mockImplementation(() => ({ updatePreferences, })); -it('Component is displayed (initializing)', async () => { - const component = render( - , - ); - // Should match snapshot - expect(component).toMatchSnapshot(); -}); - -it('Component is displayed (anonymous mode)', async () => { - Object.getPrototypeOf(window.localStorage).getItem = jest.fn(() => ('anonymous')); - const component = render( - , - ); - - await waitForElement(() => screen.getByTestId('pagination-nav')); - - // Should match snapshot +it("Component is displayed ", async () => { + mockOidcForUser(); + const component = render(); + await screen.findByText("List of surveys"); expect(component).toMatchSnapshot(); }); -it('Component is displayed (keycloak mode)', async () => { - Object.getPrototypeOf(window.localStorage).getItem = jest.fn(() => ('keycloak')); - const component = render( - , - ); - - await waitForElement(() => screen.getByTestId('pagination-nav')); - - // Should match snapshot - expect(component).toMatchSnapshot(); -}); - -it('Could not authenticate with keycloak', async () => { - Object.getPrototypeOf(window.localStorage).getItem = jest.fn(() => ('keycloak')); - - Keycloak.mockImplementation(() => ({ - init: jest.fn(() => (Promise.resolve(false))), - updateToken: (() => ({ error: (() => {}) })), - tokenParsed: { exp: 300 }, - timeSkew: 0, - })); - - const component = render( - , - ); - - // Should match snapshot +it("Component is not displayed ", async () => { + mockOidcFailed(); + const component = render(); expect(component).toMatchSnapshot(); }); diff --git a/src/components/App/AppHOC.jsx b/src/components/App/AppHOC.jsx deleted file mode 100644 index 7e18df6..0000000 --- a/src/components/App/AppHOC.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import App from "./App"; -import { - useAccessToken, - useIsAuthenticated, -} from "../../Authentication/useAuth"; -import D from "../../i18n"; - -export const AppHOC = () => { - const isAuthenticated = useIsAuthenticated(); - - const token = useAccessToken(); - - if (!isAuthenticated) { - return
{D.initializationFailed}
; - } - - return ; -}; diff --git a/src/components/App/__snapshots__/App.test.jsx.snap b/src/components/App/__snapshots__/App.test.jsx.snap index beafead..6d307da 100644 --- a/src/components/App/__snapshots__/App.test.jsx.snap +++ b/src/components/App/__snapshots__/App.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Component is displayed (anonymous mode) 1`] = ` +exports[`Component is displayed 1`] = ` Object { "asFragment": [Function], "baseElement": @@ -2309,2397 +2309,19 @@ Object { } `; -exports[`Component is displayed (initializing) 1`] = ` +exports[`Component is not displayed 1`] = ` Object { "asFragment": [Function], "baseElement":
- Initializing... + Could not load settings
, "container":
- Initializing... -
-
, - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; - -exports[`Component is displayed (keycloak mode) 1`] = ` -Object { - "asFragment": [Function], - "baseElement": -
-
-
-
-
-
- -
-
- - - - - - - - -
-
-
-
- - -
-
-
-
-
- Bienvenue - Chloé -   - Dupont -
-
- 8/20/2020 -
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
- List of surveys -
- - - Display - -
-
- -
-
- - elements - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - Survey Units -
- - - - - Survey - - - - - - - Collection start date - - - - - - Collection end date - - - - - - Processing end date - - - - - - - Phase - - - - - - - Allocated - - - - - - To treat by interviewer - - - - - - To be assigned - - - - - - To follow up - - - - - - To be reviewed - - - - - - Finalized -
-
-
-
    -
  • - - 1 - - (current) - - -
  • -
-
-
-
-
-
-
- , - "container":
-
-
-
-
-
- -
-
- - - - - - - - -
-
-
-
- - -
-
-
-
-
- Bienvenue - Chloé -   - Dupont -
-
- 8/20/2020 -
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
- List of surveys -
- - - Display - -
-
- -
-
- - elements - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - Survey Units -
- - - - - Survey - - - - - - - Collection start date - - - - - - Collection end date - - - - - - Processing end date - - - - - - - Phase - - - - - - - Allocated - - - - - - To treat by interviewer - - - - - - To be assigned - - - - - - To follow up - - - - - - To be reviewed - - - - - - Finalized -
-
-
-
    -
  • - - 1 - - (current) - - -
  • -
-
-
-
-
-
-
, - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; - -exports[`Could not authenticate with keycloak 1`] = ` -Object { - "asFragment": [Function], - "baseElement": -
-
- Initializing... -
-
- , - "container":
-
- Initializing... + Could not load settings
, "debug": [Function], diff --git a/src/index.js b/src/index.js index d226ccc..0b763ae 100644 --- a/src/index.js +++ b/src/index.js @@ -1,31 +1,57 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; -import {AppHOC} from './components/App/AppHOC'; import 'bootstrap/dist/css/bootstrap.min.css'; import 'font-awesome/css/font-awesome.min.css'; import 'react-notifications/lib/notifications.css'; import 'whatwg-fetch'; +import { initializeOidc } from './Authentication/useAuth'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { AuthProvider } from "./Authentication/useAuth"; +import { App } from "./components/App/App" -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000, - refetchOnWindowFocus: false, - retry: false, +async function main() { + // Load OIDC configuration + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + const response = await fetch(`${publicUrl.origin}/configuration.json`) + const configuration = await response.json(); + + // Inject configuration infos in the localStorage + window.localStorage.setItem( + 'AUTHENTICATION_MODE', + configuration.AUTHENTICATION_MODE, + ); + window.localStorage.setItem('PEARL_JAM_URL', configuration.PEARL_JAM_URL); + window.localStorage.setItem('QUEEN_URL_BACK_END', configuration.QUEEN_URL_BACK_END); + window.localStorage.setItem('QUEEN_URL_FRONT_END', configuration.QUEEN_URL_FRONT_END); + + // Initialize OIDC globally to use it later + const {OidcProvider} = initializeOidc({ + issuerUri: configuration.ISSUER_URI, + clientId: configuration.OIDC_CLIENT_ID, + publicUrl: "/", + }) + + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000, + refetchOnWindowFocus: false, + retry: false, + }, }, - }, -}); + }); + + ReactDOM.render( + + + + + + + , + document.getElementById('root'), + ); +} + +main(); -ReactDOM.render( - - - - - - - , - document.getElementById('root'), -); diff --git a/src/utils/Service.test.js b/src/utils/Service.test.js index e52c593..d989b6a 100644 --- a/src/utils/Service.test.js +++ b/src/utils/Service.test.js @@ -26,7 +26,7 @@ const { const service = new Service(); it('Test option creation', async () => { - const s = new Service({ token: 'ABC' }); + const s = new Service('ABC'); // Should return correct options expect(s.makeOptions()).toEqual({ headers: { map: { authorization: 'Bearer ABC', 'content-type': 'application/json' } } }); });