Skip to content

Commit

Permalink
Merge pull request #368 from DLR-SC/feature/oauth
Browse files Browse the repository at this point in the history
Feature/oauth
  • Loading branch information
Kanakanajm authored Jul 2, 2024
2 parents d7dd9b8 + 2477dcb commit 7a7c4de
Show file tree
Hide file tree
Showing 18 changed files with 236 additions and 49 deletions.
13 changes: 13 additions & 0 deletions frontend/.env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,16 @@

# This is for development only. For production you need to replace this with the actual URL.
VITE_API_URL=http://localhost:8000

# IDP Settings

# IDP URL
VITE_OAUTH_API_URL=https://lokiam.de

# IDP Client ID
# 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
2 changes: 2 additions & 0 deletions frontend/locales/de-global.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions frontend/locales/en-global.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
9 changes: 9 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
52 changes: 27 additions & 25 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -28,31 +28,33 @@ export default function App(): JSX.Element {
return (
<Suspense fallback='loading'>
<Provider store={Store}>
<ThemeProvider theme={Theme}>
<PersistGate loading={null} persistor={Persistor}>
<I18nextProvider i18n={i18n}>
<MUILocalization>
<Initializer />
<Box id='app' display='flex' flexDirection='column' sx={{height: '100%', width: '100%'}}>
<TopBar />
<Box
id='app-content'
sx={{
display: 'flex',
flexDirection: 'row',
flexGrow: 1,
alignItems: 'stretch',
width: '100%',
}}
>
<Sidebar />
<MainContent />
<AuthProvider>
<ThemeProvider theme={Theme}>
<PersistGate loading={null} persistor={Persistor}>
<I18nextProvider i18n={i18n}>
<MUILocalization>
<Initializer />
<Box id='app' display='flex' flexDirection='column' sx={{height: '100%', width: '100%'}}>
<TopBar />
<Box
id='app-content'
sx={{
display: 'flex',
flexDirection: 'row',
flexGrow: 1,
alignItems: 'stretch',
width: '100%',
}}
>
<Sidebar />
<MainContent />
</Box>
</Box>
</Box>
</MUILocalization>
</I18nextProvider>
</PersistGate>
</ThemeProvider>
</MUILocalization>
</I18nextProvider>
</PersistGate>
</ThemeProvider>
</AuthProvider>
</Provider>
</Suspense>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<I18nextProvider i18n={i18n}>
<ApplicationMenu />
<Provider store={Store}>
<ApplicationMenu />
</Provider>
</I18nextProvider>
);
const menu = screen.getByLabelText('topBar.menu.label');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<I18nextProvider i18n={i18n}>
<Suspense>
<ApplicationMenu />
<Provider store={Store}>
<ApplicationMenu />
</Provider>
</Suspense>
</I18nextProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -45,7 +46,9 @@ describe('AttributionDialog', () => {
render(
<I18nextProvider i18n={i18n}>
<Suspense>
<ApplicationMenu />
<Provider store={Store}>
<ApplicationMenu />
</Provider>
</Suspense>
</I18nextProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<I18nextProvider i18n={i18n}>
<Suspense>
<ApplicationMenu />
<Provider store={Store}>
<ApplicationMenu />
</Provider>
</Suspense>
</I18nextProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<I18nextProvider i18n={i18n}>
<Suspense>
<ApplicationMenu />
<Provider store={Store}>
<ApplicationMenu />
</Provider>
</Suspense>
</I18nextProvider>
);
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/__tests__/components/TopBar/TopBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<I18nextProvider i18n={i18n}>
<TopBar />
<Provider store={Store}>
<TopBar />
</Provider>
</I18nextProvider>
);
screen.getByAltText('topBar.icon-alt');
Expand Down
47 changes: 47 additions & 0 deletions frontend/src/components/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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';
import {useAppSelector} from 'store/hooks';

interface AuthProviderProps {
children: ReactNode;
}

function AuthProvider({children}: AuthProviderProps) {
const realm = useAppSelector((state) => state.realm.name);
let authConfig: TAuthConfig;

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: 'redirect-uri-placeholder',
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:
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,
};
}

return <OAuth2WithPkceProvider authConfig={authConfig}>{children}</OAuth2WithPkceProvider>;
}

export default AuthProvider;
37 changes: 25 additions & 12 deletions frontend/src/components/TopBar/ApplicationMenu.tsx
Original file line number Diff line number Diff line change
@@ -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'));
Expand All @@ -27,13 +27,20 @@ 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<IAuthContext>(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<Element | null>(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) => {
Expand All @@ -48,7 +55,13 @@ 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. */
Expand Down Expand Up @@ -93,7 +106,13 @@ export default function ApplicationMenu(): JSX.Element {
<MenuIcon />
</Button>
<Menu id='application-menu' anchorEl={anchorElement} open={Boolean(anchorElement)} onClose={closeMenu}>
<MenuItem onClick={loginClicked}>{t('topBar.menu.login')}</MenuItem>
{isAuthenticated ? (
<MenuItem onClick={logoutClicked}>{t('topBar.menu.logout')}</MenuItem>
) : (
<MenuItem onClick={loginClicked} disabled={loginDisabled}>
{t('topBar.menu.login')}
</MenuItem>
)}
<Divider />
<MenuItem onClick={imprintClicked}>{t('topBar.menu.imprint')}</MenuItem>
<MenuItem onClick={privacyPolicyClicked}>{t('topBar.menu.privacy-policy')}</MenuItem>
Expand Down Expand Up @@ -121,12 +140,6 @@ export default function ApplicationMenu(): JSX.Element {
<Dialog maxWidth='lg' fullWidth={true} open={changelogOpen} onClose={() => setChangelogOpen(false)}>
<ChangelogDialog />
</Dialog>

<Snackbar open={snackbarOpen} autoHideDuration={5000} onClose={() => setSnackbarOpen(false)}>
<Alert onClose={() => setSnackbarOpen(false)} severity='info'>
{t('WIP')}
</Alert>
</Snackbar>
</Box>
);
}
Loading

0 comments on commit 7a7c4de

Please sign in to comment.