Skip to content

Commit dd95daf

Browse files
committed
feat: idle user 60mins logout
1 parent 48fe34c commit dd95daf

11 files changed

Lines changed: 129 additions & 8 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ REACT_APP_OIDC_AUDIENCES=kukkuu-api-test
99
REACT_APP_API_URI=https://kukkuu.api.test.hel.ninja/graphql
1010
REACT_APP_SENTRY_DSN=https://c89ee3f57dc94ffd940d1df1a353b97f@sentry.hel.ninja/55
1111
REACT_APP_IS_TEST_ENVIRONMENT=0
12+
REACT_APP_IDLE_TIMEOUT_IN_MS=3600000
1213

1314
BROWSER_TESTS_UID=
1415
BROWSER_TESTS_PWD=

.env.local.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ REACT_APP_OIDC_SCOPE="openid profile email"
88
REACT_APP_OIDC_AUDIENCES=kukkuu-api-dev
99
REACT_APP_API_URI=http://localhost:8081/graphql
1010
REACT_APP_SENTRY_DSN=
11+
REACT_APP_IDLE_TIMEOUT_IN_MS=3600000

.env.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ REACT_APP_OIDC_SERVER_TYPE=KEYCLOAK
1010
REACT_APP_API_URI=http://localhost:8081/graphql
1111
REACT_APP_SENTRY_DSN=https://c89ee3f57dc94ffd940d1df1a353b97f@sentry.hel.ninja/55
1212
REACT_APP_IS_TEST_ENVIRONMENT=0
13+
REACT_APP_IDLE_TIMEOUT_IN_MS=3600000
1314

1415
BROWSER_TESTS_ENV_URL=http://localhost:3001
1516
BROWSER_TESTS_JWT_SIGN_SECRET=

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ ARG REACT_APP_FEATURE_FLAG_EXTERNAL_TICKET_SYSTEM_SUPPORT
6969
ARG REACT_APP_BUILDTIME
7070
ARG REACT_APP_RELEASE
7171
ARG REACT_APP_COMMITHASH
72+
ARG REACT_APP_IDLE_TIMEOUT_IN_MS
7273

7374
# Use template and inject the environment variables into .prod/nginx.conf
7475
ENV REACT_APP_BUILDTIME=${REACT_APP_BUILDTIME:-""}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"react": "^18.2.0",
7777
"react-admin": "^4.16.10",
7878
"react-dom": "^18.2.0",
79+
"react-idle-timer": "^5.7.2",
7980
"react-scripts": "^5.0.1",
8081
"typescript": "^5.3.3",
8182
"yup": "^1.3.2"

src/common/components/appBar/KukkuuAppBar.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { makeStyles } from '@mui/styles';
77
import Config from '../../../domain/config';
88
import ProfileProjectDropdown from '../../../domain/profile/ProfileProjectDropdown';
99
import AppTitle from '../appTitle/AppTitle';
10+
import IdleTimer from '../../../domain/authentication/IdleTimer';
1011

1112
const useStyles = makeStyles({
1213
title: {
@@ -28,17 +29,22 @@ const KukkuuAppBar = ({ className }: { className?: string }) => {
2829

2930
const isTestEnvironment = Config.IS_TEST_ENVIRONMENT;
3031

32+
/* note: the idle timer is placed here as its the first available shared place for components
33+
due to the react-admin architecture
34+
*/
3135
return (
3236
<AppBar
3337
className={classNames(className, {
3438
[classes.highlight]: isTestEnvironment,
3539
})}
3640
>
37-
<Typography variant="h6" color="inherit" className={classes.title}>
38-
<AppTitle />
39-
</Typography>
40-
<div className={classes.spacer} />
41-
<ProfileProjectDropdown />
41+
<IdleTimer>
42+
<Typography variant="h6" color="inherit" className={classes.title}>
43+
<AppTitle />
44+
</Typography>
45+
<div className={classes.spacer} />
46+
<ProfileProjectDropdown />
47+
</IdleTimer>
4248
</AppBar>
4349
);
4450
};

src/domain/application/AppConfig.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,17 @@ class AppConfig {
154154
* */
155155
static get oidcSessionPollerIntervalInMs(): number {
156156
return (
157-
Number(process.env.REACT_APP_OIDC_SESSION_POLLING_INTERVAL_MS) ?? 60000
157+
Number(process.env.REACT_APP_OIDC_SESSION_POLLING_INTERVAL_MS) || 60000
158158
);
159159
}
160+
161+
/**
162+
* Read env variable `REACT_APP_IDLE_TIMEOUT_IN_MS`.
163+
* Defaults to 60 minutes.
164+
* */
165+
static get userIdleTimeoutInMs(): number {
166+
return Number(process.env.REACT_APP_IDLE_TIMEOUT_IN_MS) || 3_600_000;
167+
}
160168
}
161169

162170
// Accept both variable and name so that variable can be correctly replaced

src/domain/application/__tests__/App.test.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
import { MessageChannel } from 'worker_threads';
2+
13
import React from 'react';
2-
import { render } from '@testing-library/react';
4+
import { render, cleanup } from '@testing-library/react';
35

46
import App from '../App';
57

68
// Ignore unpreventable act errors
79
let console: any;
810

911
beforeAll(() => {
12+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
13+
// @ts-ignore
14+
global.MessageChannel = MessageChannel;
1015
console = global.console;
11-
1216
global.console = {
1317
...console,
1418
error: () => {
@@ -19,6 +23,7 @@ beforeAll(() => {
1923

2024
afterAll(() => {
2125
global.console = console;
26+
cleanup();
2227
});
2328

2429
test('renders without crashing', () => {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
import { IdleTimerProvider } from 'react-idle-timer';
3+
4+
import authService from './authService';
5+
import AppConfig from '../application/AppConfig';
6+
7+
type IdleTimerProps = { children: React.ReactNode };
8+
9+
function IdleTimer({ children }: IdleTimerProps) {
10+
const onIdle = () => {
11+
const isAuthenticated = authService.isAuthenticated();
12+
if (isAuthenticated) {
13+
authService.logout();
14+
}
15+
};
16+
17+
return (
18+
<IdleTimerProvider
19+
timeout={AppConfig.userIdleTimeoutInMs || 3_600_000}
20+
onIdle={onIdle}
21+
name="kukkuu-admin-idle-timer"
22+
startOnMount
23+
crossTab
24+
>
25+
{children}
26+
</IdleTimerProvider>
27+
);
28+
}
29+
30+
export default IdleTimer;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { MessageChannel } from 'worker_threads';
2+
3+
import React from 'react';
4+
import {
5+
render,
6+
cleanup,
7+
act,
8+
fireEvent,
9+
waitFor,
10+
} from '@testing-library/react';
11+
12+
import authService from '../../authentication/authService';
13+
import IdleTimer from '../IdleTimer';
14+
15+
const originalEnv = process.env;
16+
17+
beforeAll(() => {
18+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
19+
// @ts-ignore
20+
global.MessageChannel = MessageChannel;
21+
process.env = {
22+
...originalEnv,
23+
REACT_APP_IDLE_TIMEOUT_IN_MS: '3600000',
24+
};
25+
jest.useFakeTimers();
26+
});
27+
28+
afterAll(() => {
29+
cleanup();
30+
process.env = originalEnv;
31+
jest.useFakeTimers();
32+
});
33+
34+
test('check idle timer has logged out after 60min and 1s', async () => {
35+
render(<IdleTimer />);
36+
const start = Date.now();
37+
38+
act(() => {
39+
jest.setSystemTime(start + 1000 * 60 * 60 + 1);
40+
fireEvent.focus(document);
41+
});
42+
43+
jest.spyOn(authService, 'isAuthenticated').mockReturnValueOnce(true);
44+
jest.spyOn(authService, 'logout');
45+
await waitFor(() => {
46+
expect(authService.logout()).resolves.toEqual(1);
47+
});
48+
});
49+
50+
test('check idle timer has not logged out after 50min', async () => {
51+
render(<IdleTimer />);
52+
const start = Date.now();
53+
54+
act(() => {
55+
jest.setSystemTime(start + 1000 * 60 * 50);
56+
fireEvent.focus(document);
57+
});
58+
59+
jest.spyOn(authService, 'isAuthenticated').mockReturnValueOnce(true);
60+
jest.spyOn(authService, 'logout');
61+
expect(authService.logout()).resolves.toEqual(0);
62+
});

0 commit comments

Comments
 (0)