-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #133 from adhocteam/main
Logout when inactive
- Loading branch information
Showing
16 changed files
with
326 additions
and
339 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
BACKEND_PROXY=http://localhost:8080 | ||
REACT_APP_INACTIVE_MODAL_TIMEOUT=1500000 | ||
REACT_APP_SESSION_TIMEOUT=1800000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* | ||
Displays a modal after $modalTimeout milliseconds if the user is inactive. If | ||
the user becomes active when the modal is shown it will disappear. After $logoutTimeout | ||
milliseconds the logout prop is called. | ||
*/ | ||
|
||
import React, { useState, useEffect } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { useIdleTimer } from 'react-idle-timer'; | ||
import { | ||
Button, Modal, connectModal, useModal, Alert, | ||
} from '@trussworks/react-uswds'; | ||
|
||
// list of events to determine activity | ||
// https://github.com/SupremeTechnopriest/react-idle-timer#default-events | ||
const EVENTS = [ | ||
'keydown', | ||
'wheel', | ||
'DOMMouseSc', | ||
'mousewheel', | ||
'mousedown', | ||
'touchstart', | ||
'touchmove', | ||
'MSPointerDown', | ||
'MSPointerMove', | ||
'visibilitychange', | ||
]; | ||
|
||
function IdleModal({ modalTimeout, logoutTimeout, logoutUser }) { | ||
const [inactiveTimeout, updateInactiveTimeout] = useState(); | ||
const { isOpen, openModal, closeModal } = useModal(); | ||
const modalVisibleTime = logoutTimeout - modalTimeout; | ||
const timeoutMinutes = Math.floor(modalVisibleTime / 1000 / 60); | ||
|
||
let timeToLogoutMsg = ''; | ||
if (timeoutMinutes < 1) { | ||
timeToLogoutMsg = 'less than a minute'; | ||
} else if (timeoutMinutes === 1) { | ||
timeToLogoutMsg = 'a minute'; | ||
} else { | ||
timeToLogoutMsg = `${timeoutMinutes} minutes`; | ||
} | ||
|
||
const Connected = connectModal(() => ( | ||
<Modal | ||
title={<h3>Are you still there?</h3>} | ||
actions={( | ||
<Button type="button"> | ||
Stay logged in | ||
</Button> | ||
)} | ||
> | ||
<Alert role="alert" type="warning"> | ||
You will be automatically logged out due to inactivity in | ||
{' '} | ||
{ timeToLogoutMsg } | ||
{' '} | ||
unless you become active again. | ||
<span className="usa-sr-only"> | ||
Press any key to continue your session | ||
</span> | ||
</Alert> | ||
</Modal> | ||
)); | ||
|
||
// Make sure we clean up any timeout functions when this component | ||
// is unmounted | ||
useEffect(() => function cleanup() { | ||
if (inactiveTimeout) { | ||
clearTimeout(inactiveTimeout); | ||
} | ||
}); | ||
|
||
const onIdle = () => { | ||
const timer = setTimeout(() => { | ||
closeModal(); | ||
logoutUser(true); | ||
}, modalVisibleTime); | ||
openModal(); | ||
updateInactiveTimeout(timer); | ||
}; | ||
|
||
const onActive = () => { | ||
closeModal(); | ||
clearTimeout(inactiveTimeout); | ||
}; | ||
|
||
useIdleTimer({ | ||
timeout: modalTimeout, | ||
onIdle, | ||
onActive, | ||
events: EVENTS, | ||
debounce: 500, | ||
}); | ||
|
||
return ( | ||
<Connected isOpen={isOpen} /> | ||
); | ||
} | ||
|
||
IdleModal.propTypes = { | ||
modalTimeout: PropTypes.number.isRequired, | ||
logoutTimeout: PropTypes.number.isRequired, | ||
logoutUser: PropTypes.func.isRequired, | ||
}; | ||
|
||
export default IdleModal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import '@testing-library/jest-dom'; | ||
import { act } from 'react-dom/test-utils'; | ||
import React from 'react'; | ||
import { render, screen, fireEvent } from '@testing-library/react'; | ||
|
||
import IdleModal from '../IdleModal'; | ||
|
||
describe('IdleModal', () => { | ||
beforeAll(() => { | ||
jest.useFakeTimers(); | ||
}); | ||
|
||
const renderIdleModal = ( | ||
logoutTimeout = 20, | ||
modalTimeout = 10, | ||
logoutUser = () => {}, | ||
) => { | ||
render( | ||
<> | ||
<div data-testid="test" /> | ||
<IdleModal | ||
logoutTimeout={logoutTimeout} | ||
modalTimeout={modalTimeout} | ||
logoutUser={logoutUser} | ||
/> | ||
</>, | ||
); | ||
}; | ||
|
||
it('modal is shown after modalTimeout', () => { | ||
renderIdleModal(); | ||
act(() => { | ||
jest.advanceTimersByTime(11); | ||
}); | ||
expect(screen.getByTestId('modal')).toBeVisible(); | ||
}); | ||
|
||
it('logout is called after logoutTimeout milliseconds of inactivity', () => { | ||
const logout = jest.fn(); | ||
renderIdleModal(20, 10, logout); | ||
act(() => { | ||
jest.advanceTimersByTime(21); | ||
}); | ||
expect(logout).toHaveBeenCalled(); | ||
}); | ||
|
||
it('modal is not shown after modalTimeout if there is activity', () => { | ||
const logout = jest.fn(); | ||
renderIdleModal(20, 10, logout); | ||
act(() => { | ||
jest.advanceTimersByTime(7); | ||
const testDiv = screen.getByTestId('test'); | ||
fireEvent.keyDown(testDiv, { key: 'Enter', code: 'Enter' }); | ||
jest.advanceTimersByTime(7); | ||
}); | ||
expect(logout).not.toHaveBeenCalled(); | ||
expect(screen.queryByTestId('modal')).toBeNull(); | ||
}); | ||
|
||
it('a shown modal is removed after action is taken', () => { | ||
const logout = jest.fn(); | ||
renderIdleModal(20, 10, logout); | ||
act(() => { | ||
jest.advanceTimersByTime(12); | ||
expect(screen.getByTestId('modal')).toBeVisible(); | ||
const testDiv = screen.getByTestId('test'); | ||
fireEvent.keyDown(testDiv, { key: 'Enter', code: 'Enter' }); | ||
}); | ||
expect(logout).not.toHaveBeenCalled(); | ||
expect(screen.queryByTestId('modal')).toBeNull(); | ||
}); | ||
|
||
describe('modal message', () => { | ||
it('shows less than a minute if logoutTimeout - modalTimeout is less than a minute', () => { | ||
renderIdleModal(20, 10); | ||
act(() => { | ||
jest.advanceTimersByTime(11); | ||
expect(screen.getByRole('alert').textContent).toContain('in less than a minute'); | ||
}); | ||
}); | ||
|
||
it('shows a minute if logoutTimeout - modalTimeout is a minute', () => { | ||
renderIdleModal(1000 * 60 + 10, 10); | ||
act(() => { | ||
jest.advanceTimersByTime(11); | ||
expect(screen.getByRole('alert').textContent).toContain('in a minute'); | ||
}); | ||
}); | ||
|
||
it('shows logoutTimeout - modalTimeout minutes when greater than one minute', () => { | ||
renderIdleModal((1000 * 60 * 5) + 10, 10); | ||
act(() => { | ||
jest.advanceTimersByTime(11); | ||
expect(screen.getByRole('alert').textContent).toContain('in 5 minutes'); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.