Skip to content

Commit

Permalink
Merge branch 'master' into keycloak-ramsons
Browse files Browse the repository at this point in the history
  • Loading branch information
zburke authored Sep 12, 2024
2 parents f694821 + f12abff commit 4176fde
Show file tree
Hide file tree
Showing 44 changed files with 1,226 additions and 539 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
* Terminate the session when the fixed-length session expires. Refs STCOR-862.
* Ensure support for the passed `tenantId` value by `useChunkedCQLFetch` for manipulations in the context of a specific tenant. Refs STCOR-873.
* Provide `key` to elements in `<SessionEventContainer>`. Refs STCOR-874.
* Correctly populate `stripes.user.user` on reload. Refs STCOR-860.
* Correctly evaluate `stripes.okapi` before rendering `<RootWithIntl>`. Refs STCOR-864.
* Change main navigation's skip link label to "Skip to main content". Refs STCOR-863.
* Invalidate `QueryClient` cache on login/logout. Refs STCOR-832.
* Ensure support for the passed `tenantId` value by `useChunkedCQLFetch` for manipulations in the context of a specific tenant. Refs STCOR-873.
* When re-authenticating after logout timeout, return to previous location. Refs STCOR-849.
* Add `nl` (Dutch, Flemish) to the supported locales. Refs STCOR-878.

## [10.1.1](https://github.com/folio-org/stripes-core/tree/v10.1.1) (2024-03-25)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.1.0...v10.1.1)
Expand Down Expand Up @@ -54,7 +61,6 @@
* Add `idName` and `limit` as passable props to `useChunkedCQLFetch`. Refs STCOR-821.
* Check for valid token before rotating during XHR send. Refs STCOR-817.
* Remove `autoComplete` from `<ForgotPassword>`, `<ForgotUsername>` fields. Refs STCOR-742.
* Use keycloak URLs in place of users-bl for tenant-switch. Refs US1153537.

## [10.0.3](https://github.com/folio-org/stripes-core/tree/v10.0.3) (2023-11-10)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.0.2...v10.0.3)
Expand Down
7 changes: 4 additions & 3 deletions src/RootWithIntl.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { StripesContext } from './StripesContext';
import { CalloutContext } from './CalloutContext';
import AuthnLogin from './components/AuthnLogin';

const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAuth, history = {} }) => {
const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAuth, history = {}, queryClient }) => {
const connect = connectFor('@folio/core', stripes.epics, stripes.logger);
const connectedStripes = stripes.clone({ connect });

Expand All @@ -66,7 +66,7 @@ const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAut
<>
<MainContainer>
<AppCtxMenuProvider>
<MainNav stripes={connectedStripes} />
<MainNav stripes={connectedStripes} queryClient={queryClient} />
{typeof connectedStripes?.config?.staleBundleWarning === 'object' && <StaleBundleWarning />}
<HandlerManager
event={events.LOGIN}
Expand All @@ -75,7 +75,7 @@ const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAut
{ (typeof connectedStripes.okapi !== 'object' || connectedStripes.discovery.isFinished) && (
<ModuleContainer id="content">
<OverlayContainer />
{connectedStripes.config.useSecureTokens && <SessionEventContainer history={history} />}
{connectedStripes.config.useSecureTokens && <SessionEventContainer history={history} queryClient={queryClient} />}
<Switch>
<TitledRoute
name="home"
Expand Down Expand Up @@ -191,6 +191,7 @@ RootWithIntl.propTypes = {
isAuthenticated: PropTypes.bool,
disableAuth: PropTypes.bool.isRequired,
history: PropTypes.shape({}),
queryClient: PropTypes.object.isRequired,
};

export default RootWithIntl;
41 changes: 35 additions & 6 deletions src/RootWithIntl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,27 @@ import { Router as DefaultRouter } from 'react-router-dom';
import { createMemoryHistory } from 'history';

import AuthnLogin from './components/AuthnLogin';
import MainNav from './components/MainNav';
import MainContainer from './components/MainContainer';
import ModuleContainer from './components/ModuleContainer';
import {
Login,
MainNav,
MainContainer,
ModuleContainer,
OverlayContainer,
StaleBundleWarning,
SessionEventContainer,
} from './components';

import RootWithIntl from './RootWithIntl';
import Stripes from './Stripes';

jest.mock('./components/AuthnLogin', () => () => '<AuthnLogin>');
jest.mock('./components/Login', () => () => '<Login>');
jest.mock('./components/MainNav', () => () => '<MainNav>');
jest.mock('./components/ModuleContainer', () => () => '<ModuleContainer>');
jest.mock('./components/OverlayContainer', () => () => '<OverlayContainer>');
jest.mock('./components/ModuleContainer', () => ({ children }) => children);
jest.mock('./components/MainContainer', () => ({ children }) => children);
jest.mock('./components/StaleBundleWarning', () => () => '<StaleBundleWarning>');
jest.mock('./components/SessionEventContainer', () => () => '<SessionEventContainer>');

const defaultHistory = createMemoryHistory();

Expand Down Expand Up @@ -82,14 +93,32 @@ describe('RootWithIntl', () => {
const stripes = new Stripes({ epics: {}, logger: {}, bindings: {}, config: {}, store, discovery: { isFinished: true } });
await render(<Harness><RootWithIntl stripes={stripes} history={defaultHistory} isAuthenticated /></Harness>);

expect(screen.getByText(/<ModuleContainer>/)).toBeInTheDocument();
expect(screen.queryByText(/<Login>/)).toBeNull();
expect(screen.queryByText(/<MainNav>/)).toBeInTheDocument();
expect(screen.getByText(/<OverlayContainer>/)).toBeInTheDocument();
});

it('if discovery is finished', async () => {
const stripes = new Stripes({ epics: {}, logger: {}, bindings: {}, config: {}, store, okapi: {}, discovery: { isFinished: true } });
await render(<Harness><RootWithIntl stripes={stripes} history={defaultHistory} isAuthenticated /></Harness>);

expect(screen.getByText(/<ModuleContainer>/)).toBeInTheDocument();
expect(screen.queryByText(/<Login>/)).toBeNull();
expect(screen.queryByText(/<MainNav>/)).toBeInTheDocument();
expect(screen.getByText(/<OverlayContainer>/)).toBeInTheDocument();
});
});

it('renders StaleBundleWarning', async () => {
const stripes = new Stripes({ epics: {}, logger: {}, bindings: {}, config: { staleBundleWarning: {} }, store, okapi: {}, discovery: { isFinished: true } });
await render(<Harness><RootWithIntl stripes={stripes} history={defaultHistory} isAuthenticated /></Harness>);

expect(screen.getByText(/<StaleBundleWarning>/)).toBeInTheDocument();
});

it('renders SessionEventContainer', async () => {
const stripes = new Stripes({ epics: {}, logger: {}, bindings: {}, config: { useSecureTokens: true }, store, okapi: {}, discovery: { isFinished: true } });
await render(<Harness><RootWithIntl stripes={stripes} history={defaultHistory} isAuthenticated /></Harness>);

expect(screen.getByText(/<SessionEventContainer>/)).toBeInTheDocument();
});
});
13 changes: 12 additions & 1 deletion src/components/LogoutTimeout/LogoutTimeout.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { useStripes } from '../../StripesContext';
import { logout } from '../../loginServices';

Check failure on line 15 in src/components/LogoutTimeout/LogoutTimeout.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'/home/runner/work/stripes-core/stripes-core/src/loginServices.js' imported multiple times

Check failure on line 15 in src/components/LogoutTimeout/LogoutTimeout.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'/home/runner/work/stripes-core/stripes-core/src/loginServices.js' imported multiple times

Check failure on line 15 in src/components/LogoutTimeout/LogoutTimeout.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'/home/runner/work/stripes-core/stripes-core/src/loginServices.js' imported multiple times

import styles from './LogoutTimeout.css';
import {
getUnauthorizedPathFromSession,
removeUnauthorizedPathFromSession,
} from '../../loginServices';

Check failure on line 21 in src/components/LogoutTimeout/LogoutTimeout.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'/home/runner/work/stripes-core/stripes-core/src/loginServices.js' imported multiple times

Check failure on line 21 in src/components/LogoutTimeout/LogoutTimeout.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'/home/runner/work/stripes-core/stripes-core/src/loginServices.js' imported multiple times

Check failure on line 21 in src/components/LogoutTimeout/LogoutTimeout.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'/home/runner/work/stripes-core/stripes-core/src/loginServices.js' imported multiple times

/**
* LogoutTimeout
Expand Down Expand Up @@ -52,6 +56,13 @@ const LogoutTimeout = () => {
return <LoadingView />;
}

const handleClick = (_e) => {
removeUnauthorizedPathFromSession();
};

const previousPath = getUnauthorizedPathFromSession();
const redirectTo = previousPath ?? '/';

return (
<main>
<div className={styles.wrapper} style={branding.style?.login ?? {}}>
Expand All @@ -68,7 +79,7 @@ const LogoutTimeout = () => {
</Row>
<Row center="xs">
<Col xs={12}>
<Button to="/"><FormattedMessage id="stripes-core.rtr.idleSession.logInAgain" /></Button>
<Button to={redirectTo} onClick={handleClick}><FormattedMessage id="stripes-core.rtr.idleSession.logInAgain" /></Button>
</Col>
</Row>
</div>
Expand Down
40 changes: 25 additions & 15 deletions src/components/LogoutTimeout/LogoutTimeout.test.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,46 @@
import { render, screen } from '@folio/jest-config-stripes/testing-library/react';
import { userEvent } from '@folio/jest-config-stripes/testing-library/user-event';

import LogoutTimeout from './LogoutTimeout';
import { useStripes } from '../../StripesContext';
import { logout } from '../../loginServices';


import { getUnauthorizedPathFromSession, setUnauthorizedPathToSession } from '../../loginServices';

jest.mock('../OrganizationLogo');
jest.mock('../../StripesContext');
jest.mock('react-router', () => ({
Redirect: () => <div>Redirect</div>,
}));

jest.mock('../../loginServices', () => ({
logout: jest.fn(() => Promise.resolve()),
}));

describe('LogoutTimeout', () => {
it('if not authenticated, renders a timeout message', async () => {
const mockUseStripes = useStripes;
mockUseStripes.mockReturnValue({ okapi: { isAuthenticated: false } });
describe('if not authenticated', () => {
it('renders a timeout message', async () => {
const mockUseStripes = useStripes;
mockUseStripes.mockReturnValue({ okapi: { isAuthenticated: false } });

render(<LogoutTimeout />);
screen.getByText('stripes-core.rtr.idleSession.sessionExpiredSoSad');
render(<LogoutTimeout />);
screen.getByText('stripes-core.rtr.idleSession.sessionExpiredSoSad');
});

it('clears previous path from storage after clicking', async () => {
const previousPath = '/monkey?bagel';
setUnauthorizedPathToSession(previousPath);
const user = userEvent.setup();
const mockUseStripes = useStripes;
mockUseStripes.mockReturnValue({ okapi: { isAuthenticated: false } });

render(<LogoutTimeout />);

await user.click(screen.getByRole('button'));

expect(getUnauthorizedPathFromSession()).toBe(null);
});
});

it('if authenticated, calls logout then renders a timeout message', async () => {
it('if authenticated, renders a redirect', async () => {
const mockUseStripes = useStripes;
mockUseStripes.mockReturnValue({ okapi: { isAuthenticated: true } });

render(<LogoutTimeout />);
expect(logout).toHaveBeenCalled();
screen.getByText('stripes-core.rtr.idleSession.sessionExpiredSoSad');
screen.getByText('Redirect');
});
});
6 changes: 5 additions & 1 deletion src/components/MainNav/MainNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ class MainNav extends Component {
}).isRequired,
modules: PropTypes.shape({
app: PropTypes.arrayOf(PropTypes.object),
})
}),
queryClient: PropTypes.object.isRequired,
};

constructor(props) {
Expand Down Expand Up @@ -93,6 +94,9 @@ class MainNav extends Component {
}
}
});

// remove QueryProvider cache to be 100% sure we're starting from a clean slate.
this.props.queryClient.removeQueries();
}

componentDidUpdate(prevProps) {
Expand Down
1 change: 1 addition & 0 deletions src/components/Root/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class Root extends Component {
isAuthenticated={isAuthenticated}
disableAuth={disableAuth}
history={history}
queryClient={this.reactQueryClient}
/>
</IntlProvider>
</QueryClientProvider>
Expand Down
30 changes: 20 additions & 10 deletions src/components/SessionEventContainer/SessionEventContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import createInactivityTimer from 'inactivity-timer';
import ms from 'ms';

import { SESSION_NAME, setUnauthorizedPathToSession } from '../../loginServices';
import { logout, SESSION_NAME, setUnauthorizedPathToSession } from '../../loginServices';
import KeepWorkingModal from './KeepWorkingModal';
import { useStripes } from '../../StripesContext';
import {
Expand All @@ -22,20 +22,26 @@ import FixedLengthSessionWarning from './FixedLengthSessionWarning';
//

// RTR error in this window: logout
export const thisWindowRtrError = (_e, stripes, history) => {
export const thisWindowRtrError = (_e, stripes, history, queryClient) => {
console.warn('rtr error; logging out'); // eslint-disable-line no-console
setUnauthorizedPathToSession();
history.push('/logout-timeout');
return logout(stripes.okapi.url, stripes.store, queryClient)
.then(() => {
history.push('/logout-timeout');
});
};

// idle session timeout in this window: logout
export const thisWindowRtrIstTimeout = (_e, stripes, history) => {
stripes.logger.log('rtr', 'idle session timeout; logging out');
setUnauthorizedPathToSession();
history.push('/logout-timeout');
return logout(stripes.okapi.url, stripes.store, queryClient)

Check failure on line 38 in src/components/SessionEventContainer/SessionEventContainer.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'queryClient' is not defined

Check failure on line 38 in src/components/SessionEventContainer/SessionEventContainer.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'queryClient' is not defined

Check failure on line 38 in src/components/SessionEventContainer/SessionEventContainer.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'queryClient' is not defined
.then(() => {
history.push('/logout-timeout');
});
};

// fixed-length session warning in this window: logout
// fixed-length session warning in this window
export const thisWindowRtrFlsWarning = (_e, stripes, setIsFlsVisible) => {
stripes.logger.log('rtr', 'fixed-length session warning');
setIsFlsVisible(true);
Expand All @@ -52,11 +58,14 @@ export const thisWindowRtrFlsTimeout = (_e, stripes, history) => {
// logout if it was a timeout event or if SESSION_NAME is being
// removed from localStorage, an indicator that logout is in-progress
// in another window and so must occur here as well
export const otherWindowStorage = (e, stripes, history) => {
export const otherWindowStorage = (e, stripes, history, queryClient) => {
if (e.key === RTR_TIMEOUT_EVENT) {
stripes.logger.log('rtr', 'idle session timeout; logging out');
setUnauthorizedPathToSession();
history.push('/logout-timeout');
return logout(stripes.okapi.url, stripes.store, queryClient)
.then(() => {
history.push('/logout-timeout');
});
} else if (!localStorage.getItem(SESSION_NAME)) {
stripes.logger.log('rtr', 'external localstorage change; logging out');
setUnauthorizedPathToSession();
Expand Down Expand Up @@ -129,7 +138,7 @@ export const thisWindowActivity = (_e, stripes, timers, broadcastChannel) => {
* @param {object} history
* @returns KeepWorkingModal or null
*/
const SessionEventContainer = ({ history }) => {
const SessionEventContainer = ({ history, queryClient }) => {
// is the "keep working?" modal visible?
const [isVisible, setIsVisible] = useState(false);

Expand Down Expand Up @@ -211,13 +220,13 @@ const SessionEventContainer = ({ history }) => {
timers.current = { showModalIT, logoutIT };

// RTR error in this window: logout
channels.window[RTR_ERROR_EVENT] = (e) => thisWindowRtrError(e, stripes, history);
channels.window[RTR_ERROR_EVENT] = (e) => thisWindowRtrError(e, stripes, history, queryClient);

// idle session timeout in this window: logout
channels.window[RTR_TIMEOUT_EVENT] = (e) => thisWindowRtrIstTimeout(e, stripes, history);

// localstorage change in another window: logout?
channels.window.storage = (e) => otherWindowStorage(e, stripes, history);
channels.window.storage = (e) => otherWindowStorage(e, stripes, history, queryClient);

// activity in another window: send keep-alive to idle-timers.
channels.bc.message = (message) => otherWindowActivity(message, stripes, timers, setIsVisible);
Expand Down Expand Up @@ -283,6 +292,7 @@ const SessionEventContainer = ({ history }) => {

SessionEventContainer.propTypes = {
history: PropTypes.object,
queryClient: PropTypes.object,
};

export default SessionEventContainer;
Loading

0 comments on commit 4176fde

Please sign in to comment.