Skip to content

Commit

Permalink
feat: idle user 60mins logout
Browse files Browse the repository at this point in the history
  • Loading branch information
melniiv committed Nov 5, 2024
1 parent 48fe34c commit dd95daf
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 8 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ REACT_APP_OIDC_AUDIENCES=kukkuu-api-test
REACT_APP_API_URI=https://kukkuu.api.test.hel.ninja/graphql
REACT_APP_SENTRY_DSN=https://[email protected]/55
REACT_APP_IS_TEST_ENVIRONMENT=0
REACT_APP_IDLE_TIMEOUT_IN_MS=3600000

BROWSER_TESTS_UID=
BROWSER_TESTS_PWD=
Expand Down
1 change: 1 addition & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ REACT_APP_OIDC_SCOPE="openid profile email"
REACT_APP_OIDC_AUDIENCES=kukkuu-api-dev
REACT_APP_API_URI=http://localhost:8081/graphql
REACT_APP_SENTRY_DSN=
REACT_APP_IDLE_TIMEOUT_IN_MS=3600000
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ REACT_APP_OIDC_SERVER_TYPE=KEYCLOAK
REACT_APP_API_URI=http://localhost:8081/graphql
REACT_APP_SENTRY_DSN=https://[email protected]/55
REACT_APP_IS_TEST_ENVIRONMENT=0
REACT_APP_IDLE_TIMEOUT_IN_MS=3600000

BROWSER_TESTS_ENV_URL=http://localhost:3001
BROWSER_TESTS_JWT_SIGN_SECRET=
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ ARG REACT_APP_FEATURE_FLAG_EXTERNAL_TICKET_SYSTEM_SUPPORT
ARG REACT_APP_BUILDTIME
ARG REACT_APP_RELEASE
ARG REACT_APP_COMMITHASH
ARG REACT_APP_IDLE_TIMEOUT_IN_MS

# Use template and inject the environment variables into .prod/nginx.conf
ENV REACT_APP_BUILDTIME=${REACT_APP_BUILDTIME:-""}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"react": "^18.2.0",
"react-admin": "^4.16.10",
"react-dom": "^18.2.0",
"react-idle-timer": "^5.7.2",
"react-scripts": "^5.0.1",
"typescript": "^5.3.3",
"yup": "^1.3.2"
Expand Down
16 changes: 11 additions & 5 deletions src/common/components/appBar/KukkuuAppBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { makeStyles } from '@mui/styles';
import Config from '../../../domain/config';
import ProfileProjectDropdown from '../../../domain/profile/ProfileProjectDropdown';
import AppTitle from '../appTitle/AppTitle';
import IdleTimer from '../../../domain/authentication/IdleTimer';

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

const isTestEnvironment = Config.IS_TEST_ENVIRONMENT;

/* note: the idle timer is placed here as its the first available shared place for components
due to the react-admin architecture
*/
return (
<AppBar
className={classNames(className, {
[classes.highlight]: isTestEnvironment,
})}
>
<Typography variant="h6" color="inherit" className={classes.title}>
<AppTitle />
</Typography>
<div className={classes.spacer} />
<ProfileProjectDropdown />
<IdleTimer>
<Typography variant="h6" color="inherit" className={classes.title}>
<AppTitle />
</Typography>
<div className={classes.spacer} />
<ProfileProjectDropdown />
</IdleTimer>
</AppBar>
);
};
Expand Down
10 changes: 9 additions & 1 deletion src/domain/application/AppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,17 @@ class AppConfig {
* */
static get oidcSessionPollerIntervalInMs(): number {
return (
Number(process.env.REACT_APP_OIDC_SESSION_POLLING_INTERVAL_MS) ?? 60000
Number(process.env.REACT_APP_OIDC_SESSION_POLLING_INTERVAL_MS) || 60000
);
}

/**
* Read env variable `REACT_APP_IDLE_TIMEOUT_IN_MS`.
* Defaults to 60 minutes.
* */
static get userIdleTimeoutInMs(): number {
return Number(process.env.REACT_APP_IDLE_TIMEOUT_IN_MS) || 3_600_000;
}
}

// Accept both variable and name so that variable can be correctly replaced
Expand Down
9 changes: 7 additions & 2 deletions src/domain/application/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { MessageChannel } from 'worker_threads';

import React from 'react';
import { render } from '@testing-library/react';
import { render, cleanup } from '@testing-library/react';

import App from '../App';

// Ignore unpreventable act errors
let console: any;

beforeAll(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.MessageChannel = MessageChannel;
console = global.console;

global.console = {
...console,
error: () => {
Expand All @@ -19,6 +23,7 @@ beforeAll(() => {

afterAll(() => {
global.console = console;
cleanup();
});

test('renders without crashing', () => {
Expand Down
30 changes: 30 additions & 0 deletions src/domain/authentication/IdleTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { IdleTimerProvider } from 'react-idle-timer';

import authService from './authService';
import AppConfig from '../application/AppConfig';

type IdleTimerProps = { children: React.ReactNode };

function IdleTimer({ children }: IdleTimerProps) {
const onIdle = () => {
const isAuthenticated = authService.isAuthenticated();
if (isAuthenticated) {
authService.logout();
}
};

return (
<IdleTimerProvider
timeout={AppConfig.userIdleTimeoutInMs || 3_600_000}
onIdle={onIdle}
name="kukkuu-admin-idle-timer"
startOnMount
crossTab
>
{children}
</IdleTimerProvider>
);
}

export default IdleTimer;
62 changes: 62 additions & 0 deletions src/domain/authentication/__tests__/idleProvider.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { MessageChannel } from 'worker_threads';

import React from 'react';
import {
render,
cleanup,
act,
fireEvent,
waitFor,
} from '@testing-library/react';

import authService from '../../authentication/authService';
import IdleTimer from '../IdleTimer';

const originalEnv = process.env;

beforeAll(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.MessageChannel = MessageChannel;
process.env = {
...originalEnv,
REACT_APP_IDLE_TIMEOUT_IN_MS: '3600000',
};
jest.useFakeTimers();
});

afterAll(() => {
cleanup();
process.env = originalEnv;
jest.useFakeTimers();
});

test('check idle timer has logged out after 60min and 1s', async () => {
render(<IdleTimer />);
const start = Date.now();

act(() => {
jest.setSystemTime(start + 1000 * 60 * 60 + 1);
fireEvent.focus(document);
});

jest.spyOn(authService, 'isAuthenticated').mockReturnValueOnce(true);
jest.spyOn(authService, 'logout');
await waitFor(() => {
expect(authService.logout()).resolves.toEqual(1);
});
});

test('check idle timer has not logged out after 50min', async () => {
render(<IdleTimer />);
const start = Date.now();

act(() => {
jest.setSystemTime(start + 1000 * 60 * 50);
fireEvent.focus(document);
});

jest.spyOn(authService, 'isAuthenticated').mockReturnValueOnce(true);
jest.spyOn(authService, 'logout');
expect(authService.logout()).resolves.toEqual(0);
});
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11411,6 +11411,11 @@ react-hook-form@^7.43.9:
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.47.0.tgz#a42f07266bd297ddf1f914f08f4b5f9783262f31"
integrity sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==

react-idle-timer@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/react-idle-timer/-/react-idle-timer-5.7.2.tgz#f506db28a86645dd1b87987116501703e512142b"
integrity sha512-+BaPfc7XEUU5JFkwZCx6fO1bLVK+RBlFH+iY4X34urvIzZiZINP6v2orePx3E6pAztJGE7t4DzvL7if2SL/0GQ==

react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
Expand Down

0 comments on commit dd95daf

Please sign in to comment.