From 3cbd5f6e8c501d0843e9a30133f6783dd1a8b519 Mon Sep 17 00:00:00 2001 From: Kanakanajm Date: Wed, 15 May 2024 13:20:58 +0200 Subject: [PATCH 01/14] :tada: Implement login with selectable realms --- frontend/.env.dev | 2 + frontend/package-lock.json | 9 ++++ frontend/package.json | 1 + frontend/src/App.tsx | 52 ++++++++++--------- frontend/src/components/AuthProvider.tsx | 29 +++++++++++ .../src/components/TopBar/ApplicationMenu.tsx | 36 ++++++++----- .../src/components/TopBar/RealmSelect.tsx | 44 ++++++++++++++++ frontend/src/components/TopBar/index.tsx | 2 + frontend/src/store/RealmSlice.ts | 25 +++++++++ frontend/src/store/index.ts | 2 + 10 files changed, 165 insertions(+), 37 deletions(-) create mode 100644 frontend/src/components/AuthProvider.tsx create mode 100644 frontend/src/components/TopBar/RealmSelect.tsx create mode 100644 frontend/src/store/RealmSlice.ts diff --git a/frontend/.env.dev b/frontend/.env.dev index 764e229f..60331321 100644 --- a/frontend/.env.dev +++ b/frontend/.env.dev @@ -3,3 +3,5 @@ # This is for development only. For production you need to replace this with the actual URL. VITE_API_URL=http://localhost:8000 +VITE_OAUTH_API_URL=https://134.94.199.133:7000 +VITE_OAUTH_CLIENT_ID=loki-front \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5fb448fc..6b955bcf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -30,6 +30,7 @@ "react-i18next": "^13.5.0", "react-lazyload": "github:twobin/react-lazyload", "react-markdown": "^9.0.1", + "react-oauth2-code-pkce": "^1.18.0", "react-redux": "^9.0.4", "react-scroll-sync": "^0.11.2", "redux": "^5.0.0", @@ -9806,6 +9807,14 @@ "react": ">=18" } }, + "node_modules/react-oauth2-code-pkce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/react-oauth2-code-pkce/-/react-oauth2-code-pkce-1.18.0.tgz", + "integrity": "sha512-odhGdt9PxfGvbVmWQHToDHux4rX2iR9Zxtmkr+VnJJNdnWcKvpgc4MEOWtdekZDXk+SORd4pVvsjdpyxNU55Dg==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-redux": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 33fc87af..dd3666b1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,6 +55,7 @@ "react-i18next": "^13.5.0", "react-lazyload": "github:twobin/react-lazyload", "react-markdown": "^9.0.1", + "react-oauth2-code-pkce": "^1.18.0", "react-redux": "^9.0.4", "react-scroll-sync": "^0.11.2", "redux": "^5.0.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b166b83d..3f29be74 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,7 +19,7 @@ import {selectDistrict} from './store/DataSelectionSlice'; import {I18nextProvider, useTranslation} from 'react-i18next'; import i18n from './util/i18n'; import {MUILocalization} from './components/shared/MUILocalization'; - +import AuthProvider from './components/AuthProvider'; /** * This is the root element of the React application. It divides the main screen area into the three main components. * The top bar, the sidebar and the main content area. @@ -28,31 +28,33 @@ export default function App(): JSX.Element { return ( - - - - - - - - - - + + + + + + + + + + + + - - - - - + + + + + ); diff --git a/frontend/src/components/AuthProvider.tsx b/frontend/src/components/AuthProvider.tsx new file mode 100644 index 00000000..14d7556c --- /dev/null +++ b/frontend/src/components/AuthProvider.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { ReactNode } from "react"; +import { AuthProvider as OAuth2WithPkceProvider, TAuthConfig } from "react-oauth2-code-pkce" +import { useAppSelector } from "store/hooks"; + +interface AuthProviderProps { + children: ReactNode; +} + +function AuthProvider({ children }: AuthProviderProps) { + const realm = useAppSelector((state) => state.realm.name); + + const authConfig: TAuthConfig = { + clientId: import.meta.env.VITE_OAUTH_CLIENT_ID, + authorizationEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/auth`, + tokenEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/token`, + redirectUri: window.location.origin, // always redirect to root + scope: 'openid profile email', // default scope without audience + autoLogin: false + } + + return ( + + { children } + + ) +} + +export default AuthProvider; \ No newline at end of file diff --git a/frontend/src/components/TopBar/ApplicationMenu.tsx b/frontend/src/components/TopBar/ApplicationMenu.tsx index e5a29af6..1d328e9a 100644 --- a/frontend/src/components/TopBar/ApplicationMenu.tsx +++ b/frontend/src/components/TopBar/ApplicationMenu.tsx @@ -1,17 +1,17 @@ // SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) // SPDX-License-Identifier: Apache-2.0 -import React, {MouseEvent} from 'react'; +import React, {MouseEvent, useContext} from 'react'; import MenuIcon from '@mui/icons-material/Menu'; import {useTranslation} from 'react-i18next'; -import Alert from '@mui/material/Alert'; import Button from '@mui/material/Button'; import Dialog from '@mui/material/Dialog'; import Divider from '@mui/material/Divider'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; -import Snackbar from '@mui/material/Snackbar'; import Box from '@mui/system/Box'; +import { useAppSelector } from 'store/hooks'; +import { AuthContext, IAuthContext } from 'react-oauth2-code-pkce'; // Let's import pop-ups only once they are opened. const ChangelogDialog = React.lazy(() => import('./PopUps/ChangelogDialog')); @@ -27,14 +27,22 @@ const AttributionDialog = React.lazy(() => import('./PopUps/AttributionDialog')) export default function ApplicationMenu(): JSX.Element { const {t} = useTranslation(); + const realm = useAppSelector((state) => state.realm.name); + const { login, token, logOut } = useContext(AuthContext) + + // user cannot login when realm is not selected + const loginDisabled = realm === ""; + // user is authenticated when token is not empty + const isAuthenticated = token !== ""; + const [anchorElement, setAnchorElement] = React.useState(null); const [imprintOpen, setImprintOpen] = React.useState(false); const [privacyPolicyOpen, setPrivacyPolicyOpen] = React.useState(false); const [accessibilityOpen, setAccessibilityOpen] = React.useState(false); const [attributionsOpen, setAttributionsOpen] = React.useState(false); const [changelogOpen, setChangelogOpen] = React.useState(false); - const [snackbarOpen, setSnackbarOpen] = React.useState(false); + /** Calling this method opens the application menu. */ const openMenu = (event: MouseEvent) => { setAnchorElement(event.currentTarget); @@ -48,9 +56,15 @@ export default function ApplicationMenu(): JSX.Element { /** This method gets called, when the login menu entry was clicked. */ const loginClicked = () => { closeMenu(); - setSnackbarOpen(true); + login(); }; + /** This method gets called, when the logout menu entry was clicked. */ + const logoutClicked = () => { + closeMenu(); + logOut(); + } + /** This method gets called, when the imprint menu entry was clicked. It opens a dialog showing the legal text. */ const imprintClicked = () => { closeMenu(); @@ -93,7 +107,11 @@ export default function ApplicationMenu(): JSX.Element { - {t('topBar.menu.login')} + { + isAuthenticated ? + Logout : + {t('topBar.menu.login')} + } {t('topBar.menu.imprint')} {t('topBar.menu.privacy-policy')} @@ -121,12 +139,6 @@ export default function ApplicationMenu(): JSX.Element { setChangelogOpen(false)}> - - setSnackbarOpen(false)}> - setSnackbarOpen(false)} severity='info'> - {t('WIP')} - - ); } diff --git a/frontend/src/components/TopBar/RealmSelect.tsx b/frontend/src/components/TopBar/RealmSelect.tsx new file mode 100644 index 00000000..8c6cdc05 --- /dev/null +++ b/frontend/src/components/TopBar/RealmSelect.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { + FormControl, + InputLabel, + Select, + OutlinedInput, + MenuItem, +} from "@mui/material"; +import { useAppDispatch, useAppSelector } from 'store/hooks'; +import { setRealm } from 'store/RealmSlice'; + +function RealmSelect() { + const realm = useAppSelector((state) => state.realm.name); + const dispatch = useAppDispatch() + + // realms are hardcoded for now + // should be fetched from keycloak + const realms = [ + "lha-a", + "lha-b", + "lha-c" + ]; + + return ( + + Realm + + + + ) +} + +export default RealmSelect; \ No newline at end of file diff --git a/frontend/src/components/TopBar/index.tsx b/frontend/src/components/TopBar/index.tsx index d8ef3615..a3dfcabc 100644 --- a/frontend/src/components/TopBar/index.tsx +++ b/frontend/src/components/TopBar/index.tsx @@ -7,6 +7,7 @@ import {useTranslation} from 'react-i18next'; import ApplicationMenu from './ApplicationMenu'; import Box from '@mui/material/Box'; import LanguagePicker from './LanguagePicker'; +import RealmSelect from './RealmSelect'; import esidLogo from '../../../assets/logo/logo-200x66.svg'; /** @@ -43,6 +44,7 @@ export default function TopBar(): JSX.Element { + ); diff --git a/frontend/src/store/RealmSlice.ts b/frontend/src/store/RealmSlice.ts new file mode 100644 index 00000000..d583c598 --- /dev/null +++ b/frontend/src/store/RealmSlice.ts @@ -0,0 +1,25 @@ +import {createSlice, PayloadAction} from '@reduxjs/toolkit'; + +export interface Realm { + name: string +} + +const initialState: Realm = { + name: localStorage.getItem("realm") || "" +} + +export const RealmSlice = createSlice({ + name: "Realm", + initialState, + reducers: { + setRealm(state, action: PayloadAction) { + // store realm in local storage + // redirects start new the browser sessions i.e. store state will be reset + localStorage.setItem("realm", action.payload) + state.name = action.payload; + } + }, +}); + +export const {setRealm} = RealmSlice.actions; +export default RealmSlice.reducer; \ No newline at end of file diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts index 729b48fb..f06dc5dc 100644 --- a/frontend/src/store/index.ts +++ b/frontend/src/store/index.ts @@ -11,6 +11,7 @@ import {persistReducer, persistStore} from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import {groupApi} from './services/groupApi'; import LayoutReducer from './LayoutSlice'; +import RealmReducer from './RealmSlice'; const persistConfig = { key: 'root', @@ -23,6 +24,7 @@ const rootReducer = combineReducers({ scenarioList: ScenarioReducer, userPreference: UserPreferenceReducer, layoutSlice: LayoutReducer, + realm: RealmReducer, [caseDataApi.reducerPath]: caseDataApi.reducer, [scenarioApi.reducerPath]: scenarioApi.reducer, [groupApi.reducerPath]: groupApi.reducer, From 3e2702be3f1e4ffd68cf5e07e2a6a330a0b2b086 Mon Sep 17 00:00:00 2001 From: Kanakanajm Date: Wed, 15 May 2024 13:29:23 +0200 Subject: [PATCH 02/14] :sparkles: Format --- frontend/src/components/AuthProvider.tsx | 41 ++++++------ .../src/components/TopBar/ApplicationMenu.tsx | 25 ++++---- .../src/components/TopBar/RealmSelect.tsx | 64 ++++++++----------- frontend/src/store/RealmSlice.ts | 14 ++-- 4 files changed, 66 insertions(+), 78 deletions(-) diff --git a/frontend/src/components/AuthProvider.tsx b/frontend/src/components/AuthProvider.tsx index 14d7556c..356c28c2 100644 --- a/frontend/src/components/AuthProvider.tsx +++ b/frontend/src/components/AuthProvider.tsx @@ -1,29 +1,26 @@ -import React from "react"; -import { ReactNode } from "react"; -import { AuthProvider as OAuth2WithPkceProvider, TAuthConfig } from "react-oauth2-code-pkce" -import { useAppSelector } from "store/hooks"; +import React from 'react'; +import {ReactNode} from 'react'; +import {AuthProvider as OAuth2WithPkceProvider, TAuthConfig} from 'react-oauth2-code-pkce'; +import {useAppSelector} from 'store/hooks'; interface AuthProviderProps { - children: ReactNode; + children: ReactNode; } -function AuthProvider({ children }: AuthProviderProps) { - const realm = useAppSelector((state) => state.realm.name); - - const authConfig: TAuthConfig = { - clientId: import.meta.env.VITE_OAUTH_CLIENT_ID, - authorizationEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/auth`, - tokenEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/token`, - redirectUri: window.location.origin, // always redirect to root - scope: 'openid profile email', // default scope without audience - autoLogin: false - } - - return ( - - { children } - - ) +function AuthProvider({children}: AuthProviderProps) { + const realm = useAppSelector((state) => state.realm.name); + + const authConfig: TAuthConfig = { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + clientId: import.meta.env.VITE_OAUTH_CLIENT_ID, + authorizationEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/auth`, + tokenEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/token`, + redirectUri: window.location.origin, // always redirect to root + scope: 'openid profile email', // default scope without audience + autoLogin: false, + }; + + return {children}; } export default AuthProvider; \ No newline at end of file diff --git a/frontend/src/components/TopBar/ApplicationMenu.tsx b/frontend/src/components/TopBar/ApplicationMenu.tsx index 1d328e9a..f1d19c7d 100644 --- a/frontend/src/components/TopBar/ApplicationMenu.tsx +++ b/frontend/src/components/TopBar/ApplicationMenu.tsx @@ -10,8 +10,8 @@ import Divider from '@mui/material/Divider'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import Box from '@mui/system/Box'; -import { useAppSelector } from 'store/hooks'; -import { AuthContext, IAuthContext } from 'react-oauth2-code-pkce'; +import {useAppSelector} from 'store/hooks'; +import {AuthContext, IAuthContext} from 'react-oauth2-code-pkce'; // Let's import pop-ups only once they are opened. const ChangelogDialog = React.lazy(() => import('./PopUps/ChangelogDialog')); @@ -28,12 +28,12 @@ export default function ApplicationMenu(): JSX.Element { const {t} = useTranslation(); const realm = useAppSelector((state) => state.realm.name); - const { login, token, logOut } = useContext(AuthContext) + const {login, token, logOut} = useContext(AuthContext); // user cannot login when realm is not selected - const loginDisabled = realm === ""; + const loginDisabled = realm === ''; // user is authenticated when token is not empty - const isAuthenticated = token !== ""; + const isAuthenticated = token !== ''; const [anchorElement, setAnchorElement] = React.useState(null); const [imprintOpen, setImprintOpen] = React.useState(false); @@ -42,7 +42,6 @@ export default function ApplicationMenu(): JSX.Element { const [attributionsOpen, setAttributionsOpen] = React.useState(false); const [changelogOpen, setChangelogOpen] = React.useState(false); - /** Calling this method opens the application menu. */ const openMenu = (event: MouseEvent) => { setAnchorElement(event.currentTarget); @@ -63,7 +62,7 @@ export default function ApplicationMenu(): JSX.Element { const logoutClicked = () => { closeMenu(); logOut(); - } + }; /** This method gets called, when the imprint menu entry was clicked. It opens a dialog showing the legal text. */ const imprintClicked = () => { @@ -107,11 +106,13 @@ export default function ApplicationMenu(): JSX.Element { - { - isAuthenticated ? - Logout : - {t('topBar.menu.login')} - } + {isAuthenticated ? ( + Logout + ) : ( + + {t('topBar.menu.login')} + + )} {t('topBar.menu.imprint')} {t('topBar.menu.privacy-policy')} diff --git a/frontend/src/components/TopBar/RealmSelect.tsx b/frontend/src/components/TopBar/RealmSelect.tsx index 8c6cdc05..58434f1d 100644 --- a/frontend/src/components/TopBar/RealmSelect.tsx +++ b/frontend/src/components/TopBar/RealmSelect.tsx @@ -1,44 +1,34 @@ import React from 'react'; -import { - FormControl, - InputLabel, - Select, - OutlinedInput, - MenuItem, -} from "@mui/material"; -import { useAppDispatch, useAppSelector } from 'store/hooks'; -import { setRealm } from 'store/RealmSlice'; +import {FormControl, InputLabel, Select, OutlinedInput, MenuItem} from '@mui/material'; +import {useAppDispatch, useAppSelector} from 'store/hooks'; +import {setRealm} from 'store/RealmSlice'; function RealmSelect() { - const realm = useAppSelector((state) => state.realm.name); - const dispatch = useAppDispatch() + const realm = useAppSelector((state) => state.realm.name); + const dispatch = useAppDispatch(); - // realms are hardcoded for now - // should be fetched from keycloak - const realms = [ - "lha-a", - "lha-b", - "lha-c" - ]; + // realms are hardcoded for now + // should be fetched from keycloak + const realms = ['lha-a', 'lha-b', 'lha-c']; - return ( - - Realm - - - - ) + return ( + + Realm + + + ); } -export default RealmSelect; \ No newline at end of file +export default RealmSelect; diff --git a/frontend/src/store/RealmSlice.ts b/frontend/src/store/RealmSlice.ts index d583c598..6694e2a3 100644 --- a/frontend/src/store/RealmSlice.ts +++ b/frontend/src/store/RealmSlice.ts @@ -1,25 +1,25 @@ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; export interface Realm { - name: string + name: string; } const initialState: Realm = { - name: localStorage.getItem("realm") || "" -} + name: localStorage.getItem('realm') || '', +}; export const RealmSlice = createSlice({ - name: "Realm", + name: 'Realm', initialState, reducers: { setRealm(state, action: PayloadAction) { // store realm in local storage // redirects start new the browser sessions i.e. store state will be reset - localStorage.setItem("realm", action.payload) + localStorage.setItem('realm', action.payload); state.name = action.payload; - } + }, }, }); export const {setRealm} = RealmSlice.actions; -export default RealmSlice.reducer; \ No newline at end of file +export default RealmSlice.reducer; From 1220e91578c97f9f0cb3cc3a2f08e6674ff95a44 Mon Sep 17 00:00:00 2001 From: Kanakanajm Date: Sat, 1 Jun 2024 16:16:45 +0200 Subject: [PATCH 03/14] :wrench: Try fix format --- frontend/.env.dev | 11 +++++++++-- frontend/src/components/AuthProvider.tsx | 12 +++++++----- frontend/src/components/TopBar/RealmSelect.tsx | 15 +++++++++++---- frontend/src/store/RealmSlice.ts | 3 +++ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/frontend/.env.dev b/frontend/.env.dev index 60331321..78f74079 100644 --- a/frontend/.env.dev +++ b/frontend/.env.dev @@ -3,5 +3,12 @@ # This is for development only. For production you need to replace this with the actual URL. VITE_API_URL=http://localhost:8000 -VITE_OAUTH_API_URL=https://134.94.199.133:7000 -VITE_OAUTH_CLIENT_ID=loki-front \ No newline at end of file + +# IDP Settings + +# IDP URL +VITE_OAUTH_API_URL=https://lokiam.de:7000 + +# IDP Client ID +# Dev only, change me for staging and production +VITE_OAUTH_CLIENT_ID=loki-front-dev \ No newline at end of file diff --git a/frontend/src/components/AuthProvider.tsx b/frontend/src/components/AuthProvider.tsx index 356c28c2..338a91b4 100644 --- a/frontend/src/components/AuthProvider.tsx +++ b/frontend/src/components/AuthProvider.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) and CISPA Helmholtz Center for Information Security +// SPDX-License-Identifier: Apache-2.0 + import React from 'react'; import {ReactNode} from 'react'; import {AuthProvider as OAuth2WithPkceProvider, TAuthConfig} from 'react-oauth2-code-pkce'; @@ -11,10 +14,9 @@ function AuthProvider({children}: AuthProviderProps) { const realm = useAppSelector((state) => state.realm.name); const authConfig: TAuthConfig = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - clientId: import.meta.env.VITE_OAUTH_CLIENT_ID, - authorizationEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/auth`, - tokenEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/token`, + clientId: `${import.meta.env.VITE_OAUTH_CLIENT_ID || ''}`, + authorizationEndpoint: `${import.meta.env.VITE_OAUTH_API_URL || ''}/realms/${realm}/protocol/openid-connect/auth`, + tokenEndpoint: `${import.meta.env.VITE_OAUTH_API_URL || ''}/realms/${realm}/protocol/openid-connect/token`, redirectUri: window.location.origin, // always redirect to root scope: 'openid profile email', // default scope without audience autoLogin: false, @@ -23,4 +25,4 @@ function AuthProvider({children}: AuthProviderProps) { return {children}; } -export default AuthProvider; \ No newline at end of file +export default AuthProvider; diff --git a/frontend/src/components/TopBar/RealmSelect.tsx b/frontend/src/components/TopBar/RealmSelect.tsx index 58434f1d..2e8d34e6 100644 --- a/frontend/src/components/TopBar/RealmSelect.tsx +++ b/frontend/src/components/TopBar/RealmSelect.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) and CISPA Helmholtz Center for Information Security +// SPDX-License-Identifier: Apache-2.0 + import React from 'react'; import {FormControl, InputLabel, Select, OutlinedInput, MenuItem} from '@mui/material'; import {useAppDispatch, useAppSelector} from 'store/hooks'; @@ -8,8 +11,12 @@ function RealmSelect() { const dispatch = useAppDispatch(); // realms are hardcoded for now - // should be fetched from keycloak - const realms = ['lha-a', 'lha-b', 'lha-c']; + // TODO: should be fetched from keycloak + const realms = [ + {id: 'lha-a', name: 'LHA A', disabled: false}, + {id: 'lha-b', name: 'LHA B', disabled: true}, + {id: 'lha-c', name: 'LHA C', disabled: true}, + ]; return ( @@ -22,8 +29,8 @@ function RealmSelect() { input={} > {realms.map((realm) => ( - - {realm} + + {realm.name} ))} diff --git a/frontend/src/store/RealmSlice.ts b/frontend/src/store/RealmSlice.ts index 6694e2a3..ba3b9982 100644 --- a/frontend/src/store/RealmSlice.ts +++ b/frontend/src/store/RealmSlice.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2024 German Aerospace Center (DLR) and CISPA Helmholtz Center for Information Security +// SPDX-License-Identifier: Apache-2.0 + import {createSlice, PayloadAction} from '@reduxjs/toolkit'; export interface Realm { From 4f86bda14ac989f6b73f7488c131e90c8e10e6d6 Mon Sep 17 00:00:00 2001 From: Jackie Date: Fri, 14 Jun 2024 11:12:05 +0200 Subject: [PATCH 04/14] :wrench: Fix provider and store missing test errors in TopBar related components --- .../TopBar/ApplicationMenu.test.tsx | 7 ++-- .../PopUps/AccessibilityDialog.test.tsx | 7 ++-- .../TopBar/PopUps/AttributionDialog.test.tsx | 7 ++-- .../TopBar/PopUps/ImprintDialog.test.tsx | 7 ++-- .../PopUps/PrivacyPolicyDialog.test.tsx | 7 ++-- .../components/TopBar/TopBar.test.tsx | 6 +++- frontend/src/components/AuthProvider.tsx | 32 ++++++++++++++----- 7 files changed, 54 insertions(+), 19 deletions(-) diff --git a/frontend/src/__tests__/components/TopBar/ApplicationMenu.test.tsx b/frontend/src/__tests__/components/TopBar/ApplicationMenu.test.tsx index 2ed732df..c2ec93fc 100644 --- a/frontend/src/__tests__/components/TopBar/ApplicationMenu.test.tsx +++ b/frontend/src/__tests__/components/TopBar/ApplicationMenu.test.tsx @@ -10,12 +10,15 @@ import i18n from '../../../util/i18nForTests'; import {I18nextProvider} from 'react-i18next'; import ApplicationMenu from '../../../components/TopBar/ApplicationMenu'; - +import {Provider} from 'react-redux'; +import {Store} from '../../../store'; describe('TopBarMenu', () => { test('open', async () => { render( - + + + ); const menu = screen.getByLabelText('topBar.menu.label'); diff --git a/frontend/src/__tests__/components/TopBar/PopUps/AccessibilityDialog.test.tsx b/frontend/src/__tests__/components/TopBar/PopUps/AccessibilityDialog.test.tsx index 95d66b53..a8b87125 100644 --- a/frontend/src/__tests__/components/TopBar/PopUps/AccessibilityDialog.test.tsx +++ b/frontend/src/__tests__/components/TopBar/PopUps/AccessibilityDialog.test.tsx @@ -9,13 +9,16 @@ import userEvent from '@testing-library/user-event'; import i18n from '../../../../util/i18nForTests'; import {I18nextProvider} from 'react-i18next'; import ApplicationMenu from '../../../../components/TopBar/ApplicationMenu'; - +import {Provider} from 'react-redux'; +import {Store} from '../../../../store'; describe('AccessibilityDialog', () => { test('PopUp', async () => { render( - + + + ); diff --git a/frontend/src/__tests__/components/TopBar/PopUps/AttributionDialog.test.tsx b/frontend/src/__tests__/components/TopBar/PopUps/AttributionDialog.test.tsx index 7fe2e93a..30db2fc0 100644 --- a/frontend/src/__tests__/components/TopBar/PopUps/AttributionDialog.test.tsx +++ b/frontend/src/__tests__/components/TopBar/PopUps/AttributionDialog.test.tsx @@ -11,7 +11,8 @@ import i18n from '../../../../util/i18nForTests'; import {I18nextProvider} from 'react-i18next'; import {forceVisible} from 'react-lazyload'; import ApplicationMenu from '../../../../components/TopBar/ApplicationMenu'; - +import {Provider} from 'react-redux'; +import {Store} from '../../../../store'; describe('AttributionDialog', () => { test('PopUp', async () => { // We mock fetch to return two entries for attributions. @@ -45,7 +46,9 @@ describe('AttributionDialog', () => { render( - + + + ); diff --git a/frontend/src/__tests__/components/TopBar/PopUps/ImprintDialog.test.tsx b/frontend/src/__tests__/components/TopBar/PopUps/ImprintDialog.test.tsx index 8aba5d62..10b817c4 100644 --- a/frontend/src/__tests__/components/TopBar/PopUps/ImprintDialog.test.tsx +++ b/frontend/src/__tests__/components/TopBar/PopUps/ImprintDialog.test.tsx @@ -10,13 +10,16 @@ import i18n from '../../../../util/i18nForTests'; import {I18nextProvider} from 'react-i18next'; import ApplicationMenu from '../../../../components/TopBar/ApplicationMenu'; - +import {Provider} from 'react-redux'; +import {Store} from '../../../../store'; describe('ImprintDialog', () => { test('PopUp', async () => { render( - + + + ); diff --git a/frontend/src/__tests__/components/TopBar/PopUps/PrivacyPolicyDialog.test.tsx b/frontend/src/__tests__/components/TopBar/PopUps/PrivacyPolicyDialog.test.tsx index 76cbedb5..9a4ece22 100644 --- a/frontend/src/__tests__/components/TopBar/PopUps/PrivacyPolicyDialog.test.tsx +++ b/frontend/src/__tests__/components/TopBar/PopUps/PrivacyPolicyDialog.test.tsx @@ -9,13 +9,16 @@ import userEvent from '@testing-library/user-event'; import i18n from '../../../../util/i18nForTests'; import {I18nextProvider} from 'react-i18next'; import ApplicationMenu from '../../../../components/TopBar/ApplicationMenu'; - +import {Provider} from 'react-redux'; +import {Store} from '../../../../store'; describe('PrivacyPolicyDialog', () => { test('PopUp', async () => { render( - + + + ); diff --git a/frontend/src/__tests__/components/TopBar/TopBar.test.tsx b/frontend/src/__tests__/components/TopBar/TopBar.test.tsx index 00c1cd04..92eca80b 100644 --- a/frontend/src/__tests__/components/TopBar/TopBar.test.tsx +++ b/frontend/src/__tests__/components/TopBar/TopBar.test.tsx @@ -9,12 +9,16 @@ import i18n from '../../../util/i18nForTests'; import TopBar from '../../../components/TopBar'; import {I18nextProvider} from 'react-i18next'; +import {Provider} from 'react-redux'; +import {Store} from '../../../store'; describe('TopBar', () => { test('icon', () => { render( - + + + ); screen.getByAltText('topBar.icon-alt'); diff --git a/frontend/src/components/AuthProvider.tsx b/frontend/src/components/AuthProvider.tsx index 338a91b4..95144fe9 100644 --- a/frontend/src/components/AuthProvider.tsx +++ b/frontend/src/components/AuthProvider.tsx @@ -12,15 +12,31 @@ interface AuthProviderProps { function AuthProvider({children}: AuthProviderProps) { const realm = useAppSelector((state) => state.realm.name); + let authConfig: TAuthConfig; - const authConfig: TAuthConfig = { - clientId: `${import.meta.env.VITE_OAUTH_CLIENT_ID || ''}`, - authorizationEndpoint: `${import.meta.env.VITE_OAUTH_API_URL || ''}/realms/${realm}/protocol/openid-connect/auth`, - tokenEndpoint: `${import.meta.env.VITE_OAUTH_API_URL || ''}/realms/${realm}/protocol/openid-connect/token`, - redirectUri: window.location.origin, // always redirect to root - scope: 'openid profile email', // default scope without audience - autoLogin: false, - }; + if (!import.meta.env.VITE_OAUTH_CLIENT_ID || !import.meta.env.VITE_OAUTH_API_URL) { + // in case required auth env vars are not set + console.warn( + 'Missing environment variables: VITE_OAUTH_CLIENT_ID or VITE_OAUTH_API_URL. Please set it to enable authentication.' + ); + authConfig = { + clientId: 'client-placeholder', + authorizationEndpoint: 'auth-endpoint-placeholder', + tokenEndpoint: 'token-endpoint-placeholder', + redirectUri: window.location.origin, + autoLogin: false, + }; + } else { + // actual auth configurations + authConfig = { + clientId: `${import.meta.env.VITE_OAUTH_CLIENT_ID}`, + authorizationEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/auth`, + tokenEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/token`, + redirectUri: window.location.origin, // always redirect to root + scope: 'openid profile email', // default scope without audience + autoLogin: false, + }; + } return {children}; } From bbb6d9ddd248eb5c299191b2a41c07f90cabb25c Mon Sep 17 00:00:00 2001 From: Jackie Date: Mon, 24 Jun 2024 10:44:25 +0200 Subject: [PATCH 05/14] :wrench: Update default IDP port --- frontend/.env.dev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/.env.dev b/frontend/.env.dev index 78f74079..ab32463b 100644 --- a/frontend/.env.dev +++ b/frontend/.env.dev @@ -7,7 +7,7 @@ VITE_API_URL=http://localhost:8000 # IDP Settings # IDP URL -VITE_OAUTH_API_URL=https://lokiam.de:7000 +VITE_OAUTH_API_URL=https://lokiam.de # IDP Client ID # Dev only, change me for staging and production From c194ffd586762e477abb9c307ee73c1512812b4a Mon Sep 17 00:00:00 2001 From: Jackie Date: Tue, 25 Jun 2024 17:44:30 +0200 Subject: [PATCH 06/14] Update frontend/src/components/AuthProvider.tsx Co-authored-by: Jonas Gilg --- frontend/src/components/AuthProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/AuthProvider.tsx b/frontend/src/components/AuthProvider.tsx index 95144fe9..132d7be6 100644 --- a/frontend/src/components/AuthProvider.tsx +++ b/frontend/src/components/AuthProvider.tsx @@ -23,7 +23,7 @@ function AuthProvider({children}: AuthProviderProps) { clientId: 'client-placeholder', authorizationEndpoint: 'auth-endpoint-placeholder', tokenEndpoint: 'token-endpoint-placeholder', - redirectUri: window.location.origin, + redirectUri: window.location.href, autoLogin: false, }; } else { From 4bff11151d6339a8b908513fc1ab20efa9c3f8a9 Mon Sep 17 00:00:00 2001 From: Jackie Date: Tue, 25 Jun 2024 17:44:40 +0200 Subject: [PATCH 07/14] Update frontend/src/components/AuthProvider.tsx Co-authored-by: Jonas Gilg --- frontend/src/components/AuthProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/AuthProvider.tsx b/frontend/src/components/AuthProvider.tsx index 132d7be6..00cc6b99 100644 --- a/frontend/src/components/AuthProvider.tsx +++ b/frontend/src/components/AuthProvider.tsx @@ -32,7 +32,7 @@ function AuthProvider({children}: AuthProviderProps) { clientId: `${import.meta.env.VITE_OAUTH_CLIENT_ID}`, authorizationEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/auth`, tokenEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/token`, - redirectUri: window.location.origin, // always redirect to root + redirectUri: window.location.href, scope: 'openid profile email', // default scope without audience autoLogin: false, }; From 4af0870c1b264095126cf46d674b6019a0b830c1 Mon Sep 17 00:00:00 2001 From: Jackie Date: Tue, 25 Jun 2024 17:45:09 +0200 Subject: [PATCH 08/14] Update frontend/src/components/TopBar/RealmSelect.tsx Co-authored-by: Jonas Gilg --- frontend/src/components/TopBar/RealmSelect.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/TopBar/RealmSelect.tsx b/frontend/src/components/TopBar/RealmSelect.tsx index 2e8d34e6..a18d1061 100644 --- a/frontend/src/components/TopBar/RealmSelect.tsx +++ b/frontend/src/components/TopBar/RealmSelect.tsx @@ -2,7 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import {FormControl, InputLabel, Select, OutlinedInput, MenuItem} from '@mui/material'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import MenuItem from '@mui/material/MenuItem'; import {useAppDispatch, useAppSelector} from 'store/hooks'; import {setRealm} from 'store/RealmSlice'; From 4517da6e1f3d0a4a322d6760c2497bf7775d650c Mon Sep 17 00:00:00 2001 From: Jackie Date: Tue, 25 Jun 2024 18:29:46 +0200 Subject: [PATCH 09/14] :wrench: Add locales for realm select and logout --- frontend/locales/de-global.json5 | 2 ++ frontend/locales/en-global.json5 | 2 ++ frontend/src/components/TopBar/ApplicationMenu.tsx | 2 +- frontend/src/components/TopBar/RealmSelect.tsx | 5 ++++- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/locales/de-global.json5 b/frontend/locales/de-global.json5 index 1785524c..96962d8c 100644 --- a/frontend/locales/de-global.json5 +++ b/frontend/locales/de-global.json5 @@ -7,9 +7,11 @@ topBar: { 'icon-alt': 'ESID Anwendungslogo', language: 'Sprache', + org: 'Organisation', menu: { label: 'Anwendungsmenü', login: 'Anmelden', + logout: 'Abmelden', imprint: 'Impressum', 'privacy-policy': 'Datenschutzerklärung', accessibility: 'Barrierefreiheit', diff --git a/frontend/locales/en-global.json5 b/frontend/locales/en-global.json5 index a61d0caf..0184c8a0 100644 --- a/frontend/locales/en-global.json5 +++ b/frontend/locales/en-global.json5 @@ -7,9 +7,11 @@ topBar: { 'icon-alt': 'ESID application logo', language: 'Language', + org: 'Organization', menu: { label: 'Application menu', login: 'Login', + logout: 'Logout', imprint: 'Imprint', 'privacy-policy': 'Privacy Policy', accessibility: 'Accessibility', diff --git a/frontend/src/components/TopBar/ApplicationMenu.tsx b/frontend/src/components/TopBar/ApplicationMenu.tsx index f1d19c7d..4e76da0a 100644 --- a/frontend/src/components/TopBar/ApplicationMenu.tsx +++ b/frontend/src/components/TopBar/ApplicationMenu.tsx @@ -107,7 +107,7 @@ export default function ApplicationMenu(): JSX.Element { {isAuthenticated ? ( - Logout + {t('topBar.menu.logout')} ) : ( {t('topBar.menu.login')} diff --git a/frontend/src/components/TopBar/RealmSelect.tsx b/frontend/src/components/TopBar/RealmSelect.tsx index a18d1061..94be034e 100644 --- a/frontend/src/components/TopBar/RealmSelect.tsx +++ b/frontend/src/components/TopBar/RealmSelect.tsx @@ -9,8 +9,11 @@ import OutlinedInput from '@mui/material/OutlinedInput'; import MenuItem from '@mui/material/MenuItem'; import {useAppDispatch, useAppSelector} from 'store/hooks'; import {setRealm} from 'store/RealmSlice'; +import {useTranslation} from 'react-i18next'; function RealmSelect() { + const {t} = useTranslation(); + const realm = useAppSelector((state) => state.realm.name); const dispatch = useAppDispatch(); @@ -24,7 +27,7 @@ function RealmSelect() { return ( - Realm + {t('topBar.org')} dispatch(setRealm(event.target.value))} - input={} - > - {realms.map((realm) => ( - - {realm.name} - - ))} - - + + + {t('topBar.org')} + + + ); } From 4074d4f38cfdde4ed1505cc2a1ebc68db114509c Mon Sep 17 00:00:00 2001 From: Jackie Date: Tue, 2 Jul 2024 11:27:59 +0200 Subject: [PATCH 13/14] Update frontend/src/store/RealmSlice.ts Remove realm select manual localStorage persist Co-authored-by: Jonas Gilg --- frontend/src/store/RealmSlice.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/store/RealmSlice.ts b/frontend/src/store/RealmSlice.ts index ba3b9982..6a52852c 100644 --- a/frontend/src/store/RealmSlice.ts +++ b/frontend/src/store/RealmSlice.ts @@ -8,7 +8,7 @@ export interface Realm { } const initialState: Realm = { - name: localStorage.getItem('realm') || '', + name: '', }; export const RealmSlice = createSlice({ @@ -16,9 +16,6 @@ export const RealmSlice = createSlice({ initialState, reducers: { setRealm(state, action: PayloadAction) { - // store realm in local storage - // redirects start new the browser sessions i.e. store state will be reset - localStorage.setItem('realm', action.payload); state.name = action.payload; }, }, From 2477dcb8e47d51dbaaf5a74ec1506ba07d4a9698 Mon Sep 17 00:00:00 2001 From: Jackie Date: Tue, 2 Jul 2024 15:15:52 +0200 Subject: [PATCH 14/14] :wrench: Add fixed redirect url --- frontend/.env.dev | 8 ++++++-- frontend/src/components/AuthProvider.tsx | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/.env.dev b/frontend/.env.dev index ab32463b..15fed202 100644 --- a/frontend/.env.dev +++ b/frontend/.env.dev @@ -10,5 +10,9 @@ VITE_API_URL=http://localhost:8000 VITE_OAUTH_API_URL=https://lokiam.de # IDP Client ID -# Dev only, change me for staging and production -VITE_OAUTH_CLIENT_ID=loki-front-dev \ No newline at end of file +# Dev only, change me for staging or production +VITE_OAUTH_CLIENT_ID=loki-front-dev + +# IDP Redirect URL +# Dev only, change me for staging or production +VITE_OAUTH_REDIRECT_URL=http://localhost:5443 \ No newline at end of file diff --git a/frontend/src/components/AuthProvider.tsx b/frontend/src/components/AuthProvider.tsx index 7d560c67..a5b5ea3c 100644 --- a/frontend/src/components/AuthProvider.tsx +++ b/frontend/src/components/AuthProvider.tsx @@ -23,7 +23,7 @@ function AuthProvider({children}: AuthProviderProps) { clientId: 'client-placeholder', authorizationEndpoint: 'auth-endpoint-placeholder', tokenEndpoint: 'token-endpoint-placeholder', - redirectUri: window.location.origin, + redirectUri: 'redirect-uri-placeholder', autoLogin: false, }; } else { @@ -32,7 +32,10 @@ function AuthProvider({children}: AuthProviderProps) { clientId: `${import.meta.env.VITE_OAUTH_CLIENT_ID}`, authorizationEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/auth`, tokenEndpoint: `${import.meta.env.VITE_OAUTH_API_URL}/realms/${realm}/protocol/openid-connect/token`, - redirectUri: window.location.origin, + redirectUri: + import.meta.env.VITE_OAUTH_REDIRECT_URL === undefined + ? window.location.origin + : `${import.meta.env.VITE_OAUTH_REDIRECT_URL}`, scope: 'openid profile email', // default scope without audience autoLogin: false, };