Skip to content
Snippets Groups Projects
Commit 9fb8fc5f authored by Jack Reed's avatar Jack Reed Committed by Chris Beer
Browse files

Add AuthenticationLogout component that logs out of service and opens a new window

parent 593b40ba
Branches
Tags
No related merge requests found
Showing
with 268 additions and 5 deletions
import React from 'react';
import { shallow } from 'enzyme';
import Fab from '@material-ui/core/Fab';
import { AuthenticationLogout } from '../../../src/components/AuthenticationLogout';
/**
* Helper function to create a shallow wrapper around AuthenticationLogout
*/
function createWrapper(props) {
return shallow(
<AuthenticationLogout
authServiceId="http://example.com/auth"
label="Log out now!"
logoutServiceId="http://example.com/logout"
status="ok"
t={key => key}
windowId="w"
{...props}
/>,
);
}
describe('AuthenticationLogout', () => {
it('when status is not ok, render fragment', () => {
const wrapper = createWrapper({ status: 'fail' });
expect(wrapper.matchesElement(<></>)).toBe(true);
});
it('renders Fab with logout label', () => {
const wrapper = createWrapper();
expect(wrapper.find(Fab).length).toBe(1);
expect(wrapper.find(Fab).text()).toBe('Log out now!');
});
it('click opens a new window to logout and resets state', () => {
const mockWindow = {};
const open = jest.fn(() => mockWindow);
const reset = jest.fn(() => {});
const wrapper = createWrapper({ depWindow: { open }, resetAuthenticationState: reset });
expect(wrapper.find(Fab).props().onClick());
expect(open).toHaveBeenCalledWith('http://example.com/logout');
expect(reset).toHaveBeenCalledWith({ authServiceId: 'http://example.com/auth' });
});
});
......@@ -5,9 +5,10 @@ import Collapse from '@material-ui/core/Collapse';
import DialogActions from '@material-ui/core/DialogActions';
import SanitizedHtml from '../../../src/containers/SanitizedHtml';
import { WindowAuthenticationControl } from '../../../src/components/WindowAuthenticationControl';
import AuthenticationLogout from '../../../src/containers/AuthenticationLogout';
/**
* Helper function to create a shallow wrapper around ErrorDialog
* Helper function to create a shallow wrapper around WindowAuthenticationControl
*/
function createWrapper(props) {
return shallow(
......@@ -27,9 +28,9 @@ function createWrapper(props) {
describe('WindowAuthenticationControl', () => {
let wrapper;
it('renders nothing if it is not degraded', () => {
it('renders AuthenticationLogout if it is not degraded', () => {
wrapper = createWrapper({ degraded: false });
expect(wrapper.matchesElement(<></>)).toBe(true);
expect(wrapper.find(AuthenticationLogout).length).toBe(1);
});
describe('with a non-interactive login', () => {
......
......@@ -135,4 +135,20 @@ describe('access tokens response reducer', () => {
},
});
});
describe('should handle RESET_AUTHENTICATION_STATE', () => {
it('does nothing if tokenServiceId is not present', () => {
expect(accessTokensReducer({}, {
tokenServiceId: 'foo',
type: ActionTypes.RESET_AUTHENTICATION_STATE,
})).toEqual({});
});
it('removes tokenServiceId', () => {
expect(accessTokensReducer({
foo: 'otherStuff',
}, {
tokenServiceId: 'foo',
type: ActionTypes.RESET_AUTHENTICATION_STATE,
})).toEqual({});
});
});
});
......@@ -103,4 +103,20 @@ describe('auth response reducer', () => {
},
});
});
describe('should handle RESET_AUTHENTICATION_STATE', () => {
it('does nothing if id is not present', () => {
expect(authReducer({}, {
id: 'foo',
type: ActionTypes.RESET_AUTHENTICATION_STATE,
})).toEqual({});
});
it('removes id', () => {
expect(authReducer({
foo: 'otherStuff',
}, {
id: 'foo',
type: ActionTypes.RESET_AUTHENTICATION_STATE,
})).toEqual({});
});
});
});
......@@ -13,6 +13,7 @@ import {
selectNextAuthService,
selectInfoResponse,
getVisibleCanvasNonTiledResources,
selectLogoutAuthService,
} from '../../../src/state/selectors/canvases';
describe('getVisibleCanvases', () => {
......@@ -314,6 +315,50 @@ describe('selectCanvasAuthService', () => {
});
});
describe('selectLogoutAuthService', () => {
it('returns a logout auth service if one exists', () => {
const logout = {
'@id': 'http://foo/logout',
profile: 'http://iiif.io/api/auth/1/logout',
};
const resource = {
service: [
{
'@id': 'login',
profile: 'http://iiif.io/api/auth/1/login',
service: [
logout,
],
},
],
};
const state = {
auth: {
login: {
ok: true,
},
},
infoResponses: {
'https://iiif.bodleian.ox.ac.uk/iiif/image/9cca8fdd-4a61-4429-8ac1-f648764b4d6d': {
json: resource,
},
},
manifests: {
a: {
json: manifestFixture001,
},
},
};
expect(
selectLogoutAuthService(
state,
{ canvasId: 'https://iiif.bodleian.ox.ac.uk/iiif/canvas/9cca8fdd-4a61-4429-8ac1-f648764b4d6d.json', manifestId: 'a' },
).id,
)
.toBe(logout['@id']);
});
});
describe('selectInfoResponse', () => {
it('returns in the info response for the first canvas resource', () => {
const resource = { some: 'resource' };
......
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Fab from '@material-ui/core/Fab';
/**
*
*/
export class AuthenticationLogout extends Component {
/** */
constructor(props) {
super(props);
this.handleLogout = this.handleLogout.bind(this);
}
/** */
handleLogout() {
const {
authServiceId, depWindow, logoutServiceId, resetAuthenticationState,
} = this.props;
(depWindow || window).open(logoutServiceId);
resetAuthenticationState({ authServiceId });
}
/** */
render() {
const {
label, status, t,
} = this.props;
if (status !== 'ok') return <></>;
return (
<Fab color="primary" variant="extended" onClick={this.handleLogout}>
{label || t('logout')}
</Fab>
);
}
}
AuthenticationLogout.propTypes = {
authServiceId: PropTypes.string,
depWindow: PropTypes.object, // eslint-disable-line react/forbid-prop-types
label: PropTypes.string,
logoutServiceId: PropTypes.string,
resetAuthenticationState: PropTypes.func.isRequired,
status: PropTypes.string,
t: PropTypes.func,
};
AuthenticationLogout.defaultProps = {
authServiceId: undefined,
depWindow: undefined,
label: undefined,
logoutServiceId: undefined,
status: undefined,
t: () => {},
};
......@@ -7,6 +7,7 @@ import DialogActions from '@material-ui/core/DialogActions';
import Typography from '@material-ui/core/Typography';
import LockIcon from '@material-ui/icons/LockSharp';
import SanitizedHtml from '../containers/SanitizedHtml';
import AuthenticationLogout from '../containers/AuthenticationLogout';
/**
*/
......@@ -67,10 +68,11 @@ export class WindowAuthenticationControl extends Component {
profile,
status,
t,
windowId,
} = this.props;
const failed = status === 'failed';
if ((!degraded || !profile) && status !== 'fetching') return <></>;
if ((!degraded || !profile) && status !== 'fetching') return <AuthenticationLogout windowId={windowId} />;
if (!this.isInteractive() && !failed) return <></>;
const { showFailureMessage, open } = this.state;
......
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import { withStyles } from '@material-ui/core';
import { withPlugins } from '../extend/withPlugins';
import {
getCurrentCanvas,
selectAuthStatus,
selectCanvasAuthService,
selectLogoutAuthService,
} from '../state/selectors';
import * as actions from '../state/actions';
import { AuthenticationLogout } from '../components/AuthenticationLogout';
/**
* mapStateToProps - to hook up connect
* @memberof App
* @private
*/
const mapStateToProps = (state, { windowId }) => {
const canvasId = (getCurrentCanvas(state, { windowId }) || {}).id;
const service = selectCanvasAuthService(state, { canvasId, windowId });
const logoutService = selectLogoutAuthService(state, { canvasId, windowId });
return {
authServiceId: service && service.id,
label: logoutService && logoutService.getLabel()[0].value,
logoutServiceId: logoutService && logoutService.id,
status: service && selectAuthStatus(state, service),
};
};
const mapDispatchToProps = {
resetAuthenticationState: actions.resetAuthenticationState,
};
const styles = {};
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withStyles(styles),
withTranslation(),
withPlugins('AuthenticationLogout'),
);
export default enhance(AuthenticationLogout);
......@@ -69,6 +69,7 @@
"links": "Links",
"listAllOpenWindows": "Jump to window",
"login": "Log in",
"logout": "Log out",
"manifestError": "The resource cannot be added:",
"maximizeWindow": "Maximize window",
"menu": "Menu",
......
......@@ -57,6 +57,7 @@ const ActionTypes = {
REQUEST_ACCESS_TOKEN: 'mirador/REQUEST_ACCESS_TOKEN',
RECEIVE_ACCESS_TOKEN: 'mirador/RECEIVE_ACCESS_TOKEN',
RECEIVE_ACCESS_TOKEN_FAILURE: 'mirador/RECEIVE_ACCESS_TOKEN_FAILURE',
RESET_AUTHENTICATION_STATE: 'mirador/RESET_AUTHENTICATION_STATE',
REQUEST_SEARCH: 'mirador/REQUEST_SEARCH',
RECEIVE_SEARCH: 'mirador/RECEIVE_SEARCH',
......
......@@ -127,3 +127,20 @@ export function resolveAccessTokenRequest({ messageId, ...json }) {
}
});
}
/**
* Resets authentication state for a token service
*/
export function resetAuthenticationState({ authServiceId }) {
return ((dispatch, getState) => {
const { accessTokens } = getState();
const currentService = Object.values(accessTokens)
.find(service => service.authId === authServiceId);
dispatch({
id: authServiceId,
tokenServiceId: currentService && currentService.id,
type: ActionTypes.RESET_AUTHENTICATION_STATE,
});
});
}
import normalizeUrl from 'normalize-url';
import { removeIn } from 'immutable';
import { Utils } from 'manifesto.js/dist-esmodule/Utils';
import ActionTypes from '../actions/action-types';
......@@ -59,6 +59,8 @@ export function accessTokensReducer(state = {}, action) {
isFetching: false,
},
};
case ActionTypes.RESET_AUTHENTICATION_STATE:
return removeIn(state, [action.tokenServiceId]);
default:
return state;
}
......
import normalizeUrl from 'normalize-url';
import { removeIn } from 'immutable';
import ActionTypes from '../actions/action-types';
import { selectNextAuthService } from '../selectors/canvases';
......@@ -51,6 +53,8 @@ export const authReducer = (state = {}, action) => {
ok: action.ok,
},
};
case ActionTypes.RESET_AUTHENTICATION_STATE:
return removeIn(state, [action.id]);
default: return state;
}
};
......@@ -266,6 +266,19 @@ export const selectCanvasAuthService = createSelector(
},
);
export const selectLogoutAuthService = createSelector(
[
selectInfoResponse,
state => state,
],
(infoResponse, state) => {
if (!infoResponse) return undefined;
const authService = selectActiveAuthService(state, infoResponse.json);
if (!authService) return undefined;
return authService.getService('http://iiif.io/api/auth/1/logout');
},
);
/** */
export function selectAuthStatus({ auth }, service) {
if (!service) return null;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment