Skip to content

Commit f489fd1

Browse files
authored
[OGUI-1752] TanStack auth setup (#3183)
* Wrap sessionService with TanStack query for unified approach among the whole application. * The code should check before every request if the token is still valid and re-request it before sending the main request if needed
1 parent b0beded commit f489fd1

File tree

9 files changed

+136
-49
lines changed

9 files changed

+136
-49
lines changed

Configuration/webapp/app/api/axiosInstance.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,26 @@
1313
*/
1414

1515
import axios from 'axios';
16+
import { getSessionData } from '~/services/session';
1617

1718
export const API_URL = 'http://localhost:8080/control/api';
1819

1920
const axiosInstance = axios.create({
2021
baseURL: API_URL,
2122
headers: {
2223
'Content-Type': 'application/json',
24+
'User-Agent': 'axios 0.21.1',
2325
},
2426
withCredentials: false,
2527
});
2628

29+
axiosInstance.interceptors.request.use(async (config) => {
30+
const { token } = await getSessionData();
31+
if (token) {
32+
config.url = `${config.url}?token=${token}`;
33+
}
34+
35+
return config;
36+
});
37+
2738
export default axiosInstance;

Configuration/webapp/app/components/config-navigator/ConfigNavigatorItem.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ interface ConfigNavigatorItemProps {
3535
* @returns {React.ReactElement} ConfigNavigatorItem
3636
*/
3737
const ConfigNavigatorItem: FC<ConfigNavigatorItemProps> = ({ title, onClick, isSelected }) => (
38-
<ListItem style={{ paddingTop: 5, paddingBottom: 5 }} className="config_navigator__item">
38+
<ListItem
39+
style={{ paddingTop: 5, paddingBottom: 5 }}
40+
className={`config_navigator__item ${isSelected ? 'config_navigator__item--selected' : ''} config_key__${title}`}
41+
>
3942
<Link to={`configuration/${BASE_CONFIGURATION_PATH}/${title}`} style={{ width: '100%' }}>
4043
<ListItemButton
4144
onClick={onClick}

Configuration/webapp/app/components/layout/content/ContentHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ export const ContentHeader: FC<ContentHeaderProps> = ({ currentPath }) => (
3939
<Typography variant="h5" className="config-page__header__text">
4040
{currentPath}
4141
</Typography>
42-
<UserSection userName="John D." />
42+
<UserSection />
4343
</Toolbar>
4444
);

Configuration/webapp/app/components/user-section/UserSection.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,19 @@
1212
* or submit itself to any jurisdiction.
1313
*/
1414

15-
import { Box, IconButton, Menu, MenuItem, Avatar } from '@mui/material';
16-
import { useState, type FC, type MouseEvent } from 'react';
15+
import { Box, IconButton, Menu, MenuItem, Avatar, Typography } from '@mui/material';
16+
import { useState, type MouseEvent } from 'react';
17+
import { useAuth } from '~/hooks/useAuth';
1718
import { getSessionData } from '~/services/session';
1819

19-
interface UserSectionProps {
20-
userName: string;
21-
}
22-
2320
/**
2421
* UserSection component
2522
* Represents a user section with an avatar and a dropdown menu for user actions.
26-
* @param {UserSectionProps} props - Component props.
2723
* @returns {React.ReactElement} UserSection
2824
*/
29-
export const UserSection: FC<UserSectionProps> = ({ userName }) => {
25+
export const UserSection = () => {
3026
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
31-
27+
const { name: userName } = useAuth();
3228
const handleClick = (event: MouseEvent<HTMLElement>) => {
3329
setAnchorEl(event.currentTarget);
3430
};
@@ -49,7 +45,7 @@ export const UserSection: FC<UserSectionProps> = ({ userName }) => {
4945
return (
5046
<Box sx={{ flexGrow: 0 }} className="user-section">
5147
<IconButton sx={{ p: 0 }} onClick={handleClick}>
52-
<Avatar>{userName[0]}</Avatar>
48+
<Avatar>{userName?.[0] ?? ''}</Avatar>
5349
</IconButton>
5450
<Menu
5551
anchorEl={anchorEl}
@@ -65,9 +61,12 @@ export const UserSection: FC<UserSectionProps> = ({ userName }) => {
6561
onClose={handleClose}
6662
className="user-section__menu"
6763
>
68-
<MenuItem onClick={displayProfileData}>Profile</MenuItem>
69-
<MenuItem onClick={handleClose}>My account</MenuItem>
70-
<MenuItem onClick={handleClose}>Logout</MenuItem>
64+
<Box sx={{ p: 1 }}>
65+
<Typography variant="h5">Welcome, {userName}!</Typography>
66+
<MenuItem onClick={displayProfileData}>Profile</MenuItem>
67+
<MenuItem onClick={handleClose}>My account</MenuItem>
68+
<MenuItem onClick={handleClose}>Logout</MenuItem>
69+
</Box>
7170
</Menu>
7271
</Box>
7372
);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @license
3+
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
4+
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
5+
* All rights not expressly granted are reserved.
6+
*
7+
* This software is distributed under the terms of the GNU General Public
8+
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
9+
*
10+
* In applying this license CERN does not waive the privileges and immunities
11+
* granted to it by virtue of its status as an Intergovernmental Organization
12+
* or submit itself to any jurisdiction.
13+
*/
14+
15+
import { useEffect, useState } from 'react';
16+
import { getSessionData } from '~/services/session';
17+
18+
export interface Session {
19+
personid: string;
20+
username: string;
21+
name: string;
22+
access: string;
23+
token: string;
24+
}
25+
26+
export const useAuth = (): Session => {
27+
const [session, setSession] = useState<Record<string, string>>({});
28+
29+
useEffect(() => {
30+
const fetchSession = async () => {
31+
const session = await getSessionData();
32+
setSession(session);
33+
};
34+
void fetchSession();
35+
}, []);
36+
37+
return session as unknown as Session;
38+
};

Configuration/webapp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"start": "react-router-serve ./build/server/index.js",
99
"typecheck": "react-router typegen && tsc",
1010
"docker:typecheck": "docker compose exec webapp npm run typecheck",
11-
"mocha": "tsx node_modules/.bin/mocha --timeout 20000 --require test/mocha-index.ts test/**/*.spec.ts",
11+
"mocha": "tsx node_modules/.bin/mocha --timeout 40000 --require test/mocha-index.ts test/**/*.spec.ts",
1212
"eslint": "eslint app/**",
1313
"eslint-fix": "eslint --fix app/**",
1414
"prettier": "prettier --write app/**",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @license
3+
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
4+
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
5+
* All rights not expressly granted are reserved.
6+
*
7+
* This software is distributed under the terms of the GNU General Public
8+
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
9+
*
10+
* In applying this license CERN does not waive the privileges and immunities
11+
* granted to it by virtue of its status as an Intergovernmental Organization
12+
* or submit itself to any jurisdiction.
13+
*/
14+
15+
import assert from 'assert';
16+
17+
const BAD_REQUEST_ERROR_CODE = 'ERR_BAD_REQUEST';
18+
19+
//test Configuration/webapp/app/api/axiosInstance.ts
20+
import axiosInstance, { API_URL } from '../../app/api/axiosInstance';
21+
22+
describe('axios instance', function () {
23+
this.timeout(20000);
24+
25+
it('should make a GET request and receive a response', async function () {
26+
const response = await axiosInstance.get(`${API_URL}/configurations`);
27+
assert.strictEqual(response.status, 200);
28+
});
29+
30+
it('should handle error response correctly', async function () {
31+
try {
32+
await axiosInstance.get(`${API_URL}/not-existing`);
33+
} catch (error: unknown) {
34+
if (typeof error === 'object' && error !== null && 'code' in error) {
35+
assert.strictEqual(error.code, BAD_REQUEST_ERROR_CODE);
36+
} else {
37+
assert.fail('Error object does not have code property');
38+
}
39+
}
40+
});
41+
});

Configuration/webapp/test/public/page-configuration-mocha.spec.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
import assert from 'assert';
1616
import { Page } from 'puppeteer';
1717
import global from '../mocha-index';
18-
import { API_URL } from '~/api/axiosInstance';
1918

20-
describe('`pageRoot` test-suite', function () {
19+
describe('`pageConfiguration` test-suite', function () {
2120
let url: string | null = null;
2221
let page: Page | null = null;
2322

@@ -35,37 +34,49 @@ describe('`pageRoot` test-suite', function () {
3534
assert.equal('Page is null', 'test suite failed');
3635
return;
3736
}
38-
const res = await fetch(`${API_URL}/configurations`);
39-
const data = await res.json();
37+
await page.goto(url, { waitUntil: 'networkidle0' });
4038

41-
const firstConfigurationRelativePath = data?.[0];
39+
const configNavigatorItems = await page.$$('.config_navigator__item--selected');
40+
assert.strictEqual(configNavigatorItems.length, 1);
4241

43-
const configUrl = `${url}/configuration/${firstConfigurationRelativePath}`;
42+
const classList = await configNavigatorItems[0].evaluate((el) => el.className.split(' '));
43+
const selectedKey = Array.from(classList)
44+
.find((className: string) => className.startsWith('config_key__'))
45+
?.split('__')[1];
4446

45-
await page.goto(configUrl, { waitUntil: 'networkidle0' });
47+
if (!selectedKey) {
48+
assert.equal('No selected key found', 'test suite failed');
49+
return;
50+
}
4651

4752
const location = await page.evaluate(() => window.location);
48-
assert.strictEqual(location.search, '');
53+
assert.strictEqual(location.pathname.includes(selectedKey), true);
4954
});
5055

5156
it('should display proper configuration page header', async function () {
5257
if (page === null || url === null) {
5358
assert.equal('Page is null', 'test suite failed');
5459
return;
5560
}
56-
const res = await fetch(`${API_URL}/configurations`);
57-
const data = await res.json();
61+
await page.goto(url, { waitUntil: 'networkidle0' });
5862

59-
const firstConfigurationRelativePath = data?.[0];
63+
const configNavigatorItems = await page.$$('.config_navigator__item--selected');
64+
assert.strictEqual(configNavigatorItems.length, 1);
6065

61-
const configUrl = `${url}/configuration/${firstConfigurationRelativePath}`;
66+
const classList = await configNavigatorItems[0].evaluate((el) => el.className.split(' '));
67+
const selectedKey = Array.from(classList)
68+
.find((className: string) => className.startsWith('config_key__'))
69+
?.split('__')[1];
6270

63-
await page.goto(configUrl, { waitUntil: 'networkidle0' });
71+
if (!selectedKey) {
72+
assert.equal('No selected key found', 'test suite failed');
73+
return;
74+
}
6475

6576
const configPageHeader = await page.$$('.config-page__header__text');
6677
assert.strictEqual(configPageHeader.length, 1);
6778

68-
const headerText = await page.evaluate((el) => el.textContent, configPageHeader[0]);
69-
assert.strictEqual(headerText, firstConfigurationRelativePath);
79+
const headerText = (await page.evaluate((el) => el.textContent, configPageHeader[0])) ?? '';
80+
assert.strictEqual(headerText.includes(selectedKey), true);
7081
});
7182
});

Configuration/webapp/test/public/page-root-mocha.spec.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import assert from 'assert';
1616
import { Page } from 'puppeteer';
1717
import global from '../mocha-index';
1818

19-
describe('`pageConfiguration` test-suite', function () {
19+
describe('`pageRoot` test-suite', function () {
2020
let url: string | null = null;
2121
let page: Page | null = null;
2222

@@ -128,23 +128,7 @@ describe('`pageConfiguration` test-suite', function () {
128128
return;
129129
}
130130

131-
const res = await fetch('http://localhost:8080/control/api/configurations');
132-
const data = await res.json();
133-
134-
const configNavigatorItems = await page.$$('.config_navigator__item');
135-
assert.strictEqual(configNavigatorItems.length, data?.length ?? 0);
136-
});
137-
138-
it('should display configurations list', async function () {
139-
if (page === null || url === null) {
140-
assert.equal('Page is null', 'test suite failed');
141-
return;
142-
}
143-
144-
const res = await fetch('http://localhost:8080/control/api/configurations');
145-
const data = await res.json();
146-
147131
const configNavigatorItems = await page.$$('.config_navigator__item');
148-
assert.strictEqual(configNavigatorItems.length, data?.length ?? 0);
132+
assert.strictEqual(configNavigatorItems.length > 0, true);
149133
});
150134
});

0 commit comments

Comments
 (0)