Skip to content

Commit

Permalink
Merge branch 'keycloak-ramsons' into STCOR-875
Browse files Browse the repository at this point in the history
  • Loading branch information
zburke authored Sep 13, 2024
2 parents e08f5de + 4176fde commit 7a368e6
Show file tree
Hide file tree
Showing 42 changed files with 1,166 additions and 524 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();
});
});
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import SessionEventContainer, {
import {
logout,
setUnauthorizedPathToSession,
SESSION_NAME
SESSION_NAME,
} from '../../loginServices';
import { RTR_TIMEOUT_EVENT } from '../Root/constants';

Expand Down Expand Up @@ -80,7 +80,6 @@ describe('SessionEventContainer event listeners', () => {
setUnauthorizedPathToSessionMock.mockReturnValue(null);

await thisWindowRtrError(null, { okapi: { url: 'http' } }, history);

expect(logout).toHaveBeenCalled();
expect(setUnauthorizedPathToSession).toHaveBeenCalled();
expect(history.push).toHaveBeenCalledWith('/logout-timeout');
Expand Down
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export { default as BadRequestScreen } from './BadRequestScreen';
export { default as NoPermissionScreen } from './NoPermissionScreen';
export { default as ResetPasswordNotAvailableScreen } from './ResetPasswordNotAvailableScreen';
export { default as SessionEventContainer } from './SessionEventContainer';
export { default as StaleBundleWarning } from './StaleBundleWarning';

export * from './ModuleHierarchy';
export * from './Namespace';
8 changes: 8 additions & 0 deletions src/helpers/hideEmail/hideEmail.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import hideEmail from './hideEmail';

describe('hideEmail', () => {
it('masks email addresses', () => {
expect(hideEmail('[email protected]')).toEqual('te**@e******.***');
expect(hideEmail('[email protected]')).toEqual('mo****@b****.*****');
});
});
29 changes: 17 additions & 12 deletions src/loginServices.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const supportedLocales = [
'ja', // japanese
'ko', // korean
'nb', // norwegian bokmål
'nl', // dutch, flemish
'nn', // norwegian nynorsk
'pl', // polish
'pt-BR', // portuguese, brazil
Expand Down Expand Up @@ -242,7 +243,7 @@ function dispatchLocale(url, store, tenant) {
mode: 'cors',
})
.then((response) => {
if (response.status === 200) {
if (response.ok) {
response.json().then((json) => {
if (json.configs?.length) {
const localeValues = JSON.parse(json.configs[0].value);
Expand All @@ -255,6 +256,7 @@ function dispatchLocale(url, store, tenant) {
}
});
}

return response;
});
}
Expand Down Expand Up @@ -321,7 +323,7 @@ export function getPlugins(okapiUrl, store, tenant) {
mode: 'cors',
})
.then((response) => {
if (response.status < 400) {
if (response.ok) {
response.json().then((json) => {
const configs = json.configs?.reduce((acc, val) => ({
...acc,
Expand Down Expand Up @@ -351,9 +353,7 @@ export function getBindings(okapiUrl, store, tenant) {
})
.then((response) => {
let bindings = {};
if (response.status >= 400) {
store.dispatch(setBindings(bindings));
} else {
if (response.ok) {
response.json().then((json) => {
const configs = json.configs;
if (Array.isArray(configs) && configs.length > 0) {
Expand All @@ -368,6 +368,8 @@ export function getBindings(okapiUrl, store, tenant) {
}
store.dispatch(setBindings(bindings));
});
} else {
store.dispatch(setBindings(bindings));
}
return response;
});
Expand Down Expand Up @@ -481,7 +483,7 @@ export function spreadUserWithPerms(userWithPerms) {
* @returns {Promise}
*/
export const IS_LOGGING_OUT = '@folio/stripes/core::Logout';
export async function logout(okapiUrl, store) {
export async function logout(okapiUrl, store, queryClient) {
// check the private-storage sentinel: if logout has already started
// in this window, we don't want to start it again.
if (sessionStorage.getItem(IS_LOGGING_OUT)) {
Expand Down Expand Up @@ -518,6 +520,9 @@ export async function logout(okapiUrl, store) {
store.dispatch(clearCurrentUser());
store.dispatch(clearOkapiToken());
store.dispatch(resetStore());

// clear react-query cache
queryClient.removeQueries();
})
// clear shared storage
.then(localforage.removeItem(SESSION_NAME))
Expand Down Expand Up @@ -640,14 +645,14 @@ export function createOkapiSession(store, tenant, token, data) {
export function getSSOEnabled(okapiUrl, store, tenant) {
return fetch(`${okapiUrl}/saml/check`, { headers: { 'X-Okapi-Tenant': tenant, 'Accept': 'application/json' } })
.then((response) => {
if (response.status >= 400) {
store.dispatch(checkSSO(false));
return null;
} else {
if (response.ok) {
return response.json()
.then((json) => {
store.dispatch(checkSSO(json.active));
});
} else {
store.dispatch(checkSSO(false));
return null;
}
})
.catch(() => {
Expand All @@ -663,7 +668,7 @@ export function getSSOEnabled(okapiUrl, store, tenant) {
* @param {object} resp fetch Response
*/
function processSSOLoginResponse(resp) {
if (resp.status < 400) {
if (resp.ok) {
resp.json().then((json) => {
const form = document.getElementById('ssoForm');
if (json.bindingMethod === 'POST') {
Expand Down Expand Up @@ -792,7 +797,7 @@ export function validateUser(okapiUrl, store, tenant, session) {
// data isn't provided by _self.
store.dispatch(setSessionData({
isAuthenticated: true,
user: data.user,
user,
perms,
tenant: sessionTenant,
token,
Expand Down
Loading

0 comments on commit 7a368e6

Please sign in to comment.