Skip to content
Snippets Groups Projects
Commit e19c7e02 authored by Chris Beer's avatar Chris Beer
Browse files

Consolidate IIIF authentication behaviors into a window-specific control + generic UI bar

parent b15cbb61
No related branches found
No related tags found
No related merge requests found
Showing
with 655 additions and 600 deletions
......@@ -2,8 +2,6 @@ import React from 'react';
import { shallow } from 'enzyme';
import PluginProvider from '../../../src/extend/PluginProvider';
import AppProviders from '../../../src/containers/AppProviders';
import AccessTokenSender from '../../../src/containers/AccessTokenSender';
import AuthenticationSender from '../../../src/containers/AuthenticationSender';
import { App } from '../../../src/components/App';
/** */
......@@ -21,7 +19,5 @@ describe('App', () => {
expect(wrapper.find(PluginProvider).length).toBe(1);
expect(wrapper.find(AppProviders).length).toBe(1);
expect(wrapper.find('Suspense').length).toBe(1);
expect(wrapper.find(AuthenticationSender).length).toBe(1);
expect(wrapper.find(AccessTokenSender).length).toBe(1);
});
});
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' });
});
});
import React from 'react';
import { shallow } from 'enzyme';
import { NewWindow } from '../../../src/components/NewWindow';
import { AuthenticationSender } from '../../../src/components/AuthenticationSender';
/**
* Helper function to create a shallow wrapper around ErrorDialog
*/
function createWrapper(props) {
return shallow(
<AuthenticationSender
t={key => key}
handleInteraction={() => {}}
{...props}
/>,
);
}
describe('AuthenticationSender', () => {
let wrapper;
it('renders nothing if there is no url', () => {
wrapper = createWrapper({});
expect(wrapper.matchesElement(<></>)).toBe(true);
});
it('renders properly', () => {
Object.defineProperty(window, 'origin', {
value: 'http://localhost',
writable: true,
});
wrapper = createWrapper({ url: 'http://example.com' });
expect(wrapper.find(NewWindow).length).toBe(1);
expect(wrapper.find(NewWindow).props().url).toBe('http://example.com?origin=http://localhost');
});
it('triggers an action when the window is unloaded', () => {
const handleInteraction = jest.fn();
wrapper = createWrapper({ handleInteraction, url: 'http://example.com' });
wrapper.find(NewWindow).simulate('close');
expect(handleInteraction).toHaveBeenCalledWith('http://example.com');
});
});
import React from 'react';
import { shallow } from 'enzyme';
import WindowAuthenticationBar from '../../../src/containers/WindowAuthenticationBar';
import { NewWindow } from '../../../src/components/NewWindow';
import { AccessTokenSender } from '../../../src/components/AccessTokenSender';
import { IIIFAuthentication } from '../../../src/components/IIIFAuthentication';
/**
* Helper function to create a shallow wrapper around IIIFAuthentication
*/
function createWrapper(props) {
return shallow(
<IIIFAuthentication
accessTokenServiceId="http://example.com/token"
authServiceId="http://example.com/auth"
failureDescription="... and this is why."
failureHeader="Login failed"
handleAuthInteraction={() => {}}
isInteractive
logoutServiceId="http://example.com/logout"
resetAuthenticationState={() => {}}
resolveAccessTokenRequest={() => {}}
resolveAuthenticationRequest={() => {}}
t={key => key}
windowId="w"
{...props}
/>,
);
}
describe('IIIFAuthentication', () => {
describe('without an auth service', () => {
it('renders nothing', () => {
const wrapper = createWrapper({ authServiceId: null });
expect(wrapper.isEmptyRender()).toBe(true);
});
});
describe('with an available auth service', () => {
it('renders a login bar', () => {
const handleAuthInteraction = jest.fn();
const wrapper = createWrapper({ handleAuthInteraction });
expect(wrapper.find(WindowAuthenticationBar).length).toBe(1);
expect(wrapper.find(WindowAuthenticationBar).simulate('confirm'));
expect(handleAuthInteraction).toHaveBeenCalledWith('w', 'http://example.com/auth');
});
it('renders nothing for a non-interactive login', () => {
const wrapper = createWrapper({ isInteractive: false });
expect(wrapper.isEmptyRender()).toBe(true);
});
});
describe('with a failed authentication', () => {
it('renders with an error message', () => {
const handleAuthInteraction = jest.fn();
const wrapper = createWrapper({ handleAuthInteraction, status: 'failed' });
expect(wrapper.find(WindowAuthenticationBar).length).toBe(1);
expect(wrapper.find(WindowAuthenticationBar).prop('confirmButton')).toEqual('retry');
expect(wrapper.find(WindowAuthenticationBar).prop('status')).toEqual('failed');
expect(wrapper.find(WindowAuthenticationBar).prop('header')).toEqual('Login failed');
expect(wrapper.find(WindowAuthenticationBar).prop('description')).toEqual('... and this is why.');
expect(wrapper.find(WindowAuthenticationBar).simulate('confirm'));
expect(handleAuthInteraction).toHaveBeenCalledWith('w', 'http://example.com/auth');
});
});
describe('in the middle of authenicating', () => {
it('does the IIIF access cookie behavior', () => {
const wrapper = createWrapper({ status: 'cookie' });
expect(wrapper.find(WindowAuthenticationBar).length).toBe(1);
expect(wrapper.find(NewWindow).length).toBe(1);
expect(wrapper.find(NewWindow).prop('url')).toContain('http://example.com/auth?origin=');
});
it('does the IIIF access token behavior', () => {
const wrapper = createWrapper({ status: 'token' });
expect(wrapper.find(WindowAuthenticationBar).length).toBe(1);
expect(wrapper.find(AccessTokenSender).length).toBe(1);
expect(wrapper.find(AccessTokenSender).prop('url')).toEqual('http://example.com/token');
});
});
describe('when logged in', () => {
it('renders a logout button', () => {
const openWindow = jest.fn();
const resetAuthenticationState = jest.fn();
const wrapper = createWrapper({ openWindow, resetAuthenticationState, status: 'ok' });
expect(wrapper.find(WindowAuthenticationBar).length).toBe(1);
expect(wrapper.find(WindowAuthenticationBar).prop('confirmButton')).toEqual('logout');
expect(wrapper.find(WindowAuthenticationBar).prop('hasLogoutService')).toEqual(true);
wrapper.find(WindowAuthenticationBar).simulate('confirm');
expect(openWindow).toHaveBeenCalledWith('http://example.com/logout', 'centerscreen');
expect(resetAuthenticationState).toHaveBeenCalledWith({
authServiceId: 'http://example.com/auth', tokenServiceId: 'http://example.com/token',
});
});
});
});
......@@ -3,7 +3,7 @@ import { shallow } from 'enzyme';
import { Window } from '../../../src/components/Window';
import WindowTopBar from '../../../src/containers/WindowTopBar';
import PrimaryWindow from '../../../src/containers/PrimaryWindow';
import WindowAuthenticationControl from '../../../src/containers/WindowAuthenticationControl';
import IIIFAuthentication from '../../../src/containers/IIIFAuthentication';
import ErrorContent from '../../../src/containers/ErrorContent';
/** create wrapper */
......@@ -34,9 +34,9 @@ describe('Window', () => {
wrapper = createWrapper();
expect(wrapper.find(PrimaryWindow)).toHaveLength(1);
});
it('renders <WindowAuthenticationControl>', () => {
it('renders <WindowAuthenticationBar>', () => {
wrapper = createWrapper();
expect(wrapper.find(WindowAuthenticationControl)).toHaveLength(1);
expect(wrapper.find(IIIFAuthentication)).toHaveLength(1);
});
it('renders manifest error', () => {
wrapper = createWrapper({ manifestError: 'Invalid JSON' });
......
import React from 'react';
import { shallow } from 'enzyme';
import Button from '@material-ui/core/Button';
import Collapse from '@material-ui/core/Collapse';
import DialogActions from '@material-ui/core/DialogActions';
import SanitizedHtml from '../../../src/containers/SanitizedHtml';
import { WindowAuthenticationBar } from '../../../src/components/WindowAuthenticationBar';
/**
* Helper function to create a shallow wrapper around AuthenticationLogout
*/
function createWrapper(props) {
return shallow(
<WindowAuthenticationBar
classes={{}}
hasLogoutService
confirmButton="Click here"
label="Log in to see more"
onConfirm={() => {}}
status="ok"
t={key => key}
windowId="w"
{...props}
/>,
);
}
describe('AuthenticationControl', () => {
it('renders nothing if the user is logged in and there is no logout service', () => {
const wrapper = createWrapper({ hasLogoutService: false });
expect(wrapper.isEmptyRender()).toBe(true);
});
it('renders a non-collapsing version if there is no description', () => {
const wrapper = createWrapper({ description: undefined, header: undefined });
expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('Log in to see more');
expect(wrapper.find(Button).children().text()).toEqual('Click here');
});
it('renders a collapsable version if there is a description', () => {
const onConfirm = jest.fn();
const wrapper = createWrapper({ description: 'long description', header: 'header', onConfirm });
expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('Log in to see more');
expect(wrapper.find(Button).at(0).find('span').text()).toEqual('continue');
// is expandable
expect(wrapper.find(Collapse).prop('in')).toEqual(false);
wrapper.find(Button).at(0).simulate('click');
expect(wrapper.find(Collapse).prop('in')).toEqual(true);
// has more information
expect(wrapper.find(Collapse).find(SanitizedHtml).at(0).props().htmlString).toEqual('header');
expect(wrapper.find(Collapse).find(SanitizedHtml).at(1).props().htmlString).toEqual('long description');
// is recollapsable
wrapper.find(DialogActions).find(Button).at(0).simulate('click');
expect(wrapper.find(Collapse).prop('in')).toEqual(false);
wrapper.find(Button).at(0).simulate('click');
// starts the auth process
wrapper.find(DialogActions).find(Button).at(1).simulate('click');
expect(onConfirm).toHaveBeenCalled();
});
it('triggers an action when the confirm button is clicked', () => {
const onConfirm = jest.fn();
const wrapper = createWrapper({
onConfirm,
});
wrapper.find(Button).simulate('click');
expect(onConfirm).toHaveBeenCalled();
});
});
import React from 'react';
import { shallow } from 'enzyme';
import Button from '@material-ui/core/Button';
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 WindowAuthenticationControl
*/
function createWrapper(props) {
return shallow(
<WindowAuthenticationControl
t={key => key}
classes={{}}
degraded
handleAuthInteraction={() => {}}
label="authenticate"
windowId="w"
profile="http://iiif.io/api/auth/1/login"
{...props}
/>,
);
}
describe('WindowAuthenticationControl', () => {
let wrapper;
it('renders AuthenticationLogout if it is not degraded', () => {
wrapper = createWrapper({ degraded: false });
expect(wrapper.find(AuthenticationLogout).length).toBe(1);
});
describe('with a non-interactive login', () => {
it('renders failure messages', () => {
wrapper = createWrapper({
degraded: true,
failureDescription: 'failure description',
failureHeader: 'failure header',
profile: 'http://iiif.io/api/auth/1/external',
status: 'failed',
});
expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('failure header');
expect(wrapper.find(SanitizedHtml).at(1).props().htmlString).toEqual('failure description');
expect(wrapper.find(DialogActions)).toHaveLength(1);
expect(wrapper.find(DialogActions).find(Button)).toHaveLength(2);
expect(wrapper.find(DialogActions).find(Button).at(1).children()
.text()).toEqual('retry');
});
});
it('renders properly', () => {
wrapper = createWrapper({ confirmLabel: 'some confirm label', description: 'some description' });
expect(wrapper.find(SanitizedHtml).at(2).props().htmlString).toEqual('some description');
expect(wrapper.find(DialogActions).find(Button)).toHaveLength(2);
expect(wrapper.find(DialogActions).find(Button).at(1).children()
.text()).toEqual('some confirm label');
});
it('hides the cancel button if there is nothing to collapose', () => {
wrapper = createWrapper({ classes: { topBar: 'topBar' }, confirmLabel: 'some confirm label' });
expect(wrapper.find('.topBar').children().find(Button).at(0)
.children()
.text()).toEqual('some confirm label');
expect(wrapper.find(DialogActions).find(Button)).toHaveLength(0);
});
it('shows the auth dialog when the login button is clicked', () => {
wrapper = createWrapper({ classes: { topBar: 'topBar' }, description: 'some description' });
wrapper.find('.topBar').props().onClick();
expect(wrapper.find(Collapse).props().in).toEqual(true);
});
it('triggers an action when the confirm button is clicked', () => {
const handleAuthInteraction = jest.fn();
wrapper = createWrapper({
confirmLabel: 'some confirm label',
description: 'some description',
handleAuthInteraction,
infoId: 'i',
serviceId: 's',
});
wrapper.instance().setState({ open: true });
expect(wrapper.find(Collapse).props().in).toEqual(true);
wrapper.find(DialogActions).find(Button).at(1).simulate('click');
expect(handleAuthInteraction).toHaveBeenCalledWith('w', 'i', 's');
});
it('displays a failure message if the login has failed', () => {
wrapper = createWrapper({
failureDescription: 'failure description',
failureHeader: 'failure header',
status: 'failed',
});
expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('failure header');
expect(wrapper.find(SanitizedHtml).at(1).props().htmlString).toEqual('failure description');
expect(wrapper.find(DialogActions).find(Button)).toHaveLength(2);
expect(wrapper.find(DialogActions).find(Button).at(1).children()
.text()).toEqual('login');
});
it('displays the login messages if the user dismisses the failure messages', () => {
wrapper = createWrapper({
failureDescription: 'failure description',
failureHeader: 'failure header',
status: 'failed',
});
expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('failure header');
expect(wrapper.find(SanitizedHtml).at(1).props().htmlString).toEqual('failure description');
wrapper.find(DialogActions).find(Button).at(0).simulate('click');
expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('authenticate');
});
});
......@@ -2,8 +2,6 @@ import React, { Component, lazy, Suspense } from 'react';
import PropTypes from 'prop-types';
import PluginProvider from '../extend/PluginProvider';
import AppProviders from '../containers/AppProviders';
import AuthenticationSender from '../containers/AuthenticationSender';
import AccessTokenSender from '../containers/AccessTokenSender';
const WorkspaceArea = lazy(() => import('../containers/WorkspaceArea'));
......@@ -22,8 +20,6 @@ export class App extends Component {
return (
<PluginProvider plugins={plugins}>
<AppProviders dndManager={dndManager}>
<AuthenticationSender />
<AccessTokenSender />
<Suspense
fallback={<div />}
>
......
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: () => {},
};
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { NewWindow } from './NewWindow';
/**
* Opens a new window for click
*/
export class AuthenticationSender extends Component {
/** */
constructor(props) {
super(props);
this.onClose = this.onClose.bind(this);
}
/** @private */
onClose() {
const { handleInteraction, url } = this.props;
handleInteraction(url);
}
/** */
render() {
const { url } = this.props;
if (!url) return <></>;
return <NewWindow name="IiifAuthenticationSender" url={`${url}?origin=${window.origin}`} features="centerscreen" onClose={this.onClose} />;
}
}
AuthenticationSender.propTypes = {
handleInteraction: PropTypes.func.isRequired,
url: PropTypes.string,
};
AuthenticationSender.defaultProps = {
url: undefined,
};
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { AccessTokenSender } from './AccessTokenSender';
import { NewWindow } from './NewWindow';
import WindowAuthenticationBar from '../containers/WindowAuthenticationBar';
/**
* Opens a new window for click
*/
export class IIIFAuthentication extends Component {
/** */
constructor(props) {
super(props);
this.performLogout = this.performLogout.bind(this);
this.onReceiveAccessTokenMessage = this.onReceiveAccessTokenMessage.bind(this);
}
/** */
onReceiveAccessTokenMessage(payload) {
const {
authServiceId, accessTokenServiceId, resolveAccessTokenRequest,
} = this.props;
resolveAccessTokenRequest(authServiceId, accessTokenServiceId, payload);
}
/** */
defaultAuthBarProps() {
const {
authServiceId, windowId, status, logoutServiceId,
} = this.props;
return {
authServiceId,
hasLogoutService: !!logoutServiceId,
status,
windowId,
};
}
/** handle the IIIF logout workflow */
performLogout() {
const {
accessTokenServiceId, authServiceId, features,
logoutServiceId, resetAuthenticationState, openWindow,
} = this.props;
openWindow(logoutServiceId, features);
resetAuthenticationState({ authServiceId, tokenServiceId: accessTokenServiceId });
}
/** Render the auth bar for logged in users */
renderLoggedIn() {
const {
isInteractive, t,
} = this.props;
if (!isInteractive) return null;
return (
<WindowAuthenticationBar
confirmButton={t('logout')}
onConfirm={this.performLogout}
{...this.defaultAuthBarProps()}
/>
);
}
/** Render whatever shows up after the interactive login attempt fails */
renderFailure() {
const {
handleAuthInteraction, failureHeader: header, failureDescription: description, t,
authServiceId, windowId,
} = this.props;
return (
<WindowAuthenticationBar
header={header}
description={description}
confirmButton={t('retry')}
onConfirm={() => handleAuthInteraction(windowId, authServiceId)}
{...this.defaultAuthBarProps()}
/>
);
}
/** Render the login bar after we're logging in */
renderLoggingInCookie() {
const {
accessTokenServiceId, authServiceId, resolveAuthenticationRequest, features,
} = this.props;
return (
<>
{this.renderLogin()}
<NewWindow name="IiifLoginSender" url={`${authServiceId}?origin=${window.origin}`} features={features} onClose={() => resolveAuthenticationRequest(authServiceId, accessTokenServiceId)} />
</>
);
}
/** Render the login bar after we're logging in */
renderLoggingInToken() {
const {
accessTokenServiceId,
} = this.props;
return (
<>
{this.renderLogin()}
<AccessTokenSender
handleAccessTokenMessage={this.onReceiveAccessTokenMessage}
url={accessTokenServiceId}
/>
</>
);
}
/** Render a login bar */
renderLogin() {
const {
confirm, description, handleAuthInteraction, header, isInteractive, label,
authServiceId, windowId,
} = this.props;
if (!isInteractive) return null;
return (
<WindowAuthenticationBar
header={header}
description={description}
label={label}
confirmButton={confirm}
onConfirm={() => handleAuthInteraction(windowId, authServiceId)}
{...this.defaultAuthBarProps()}
/>
);
}
/** */
render() {
const { authServiceId, status } = this.props;
if (!authServiceId) return null;
if (status === null) return this.renderLogin();
if (status === 'cookie') return this.renderLoggingInCookie();
if (status === 'token') return this.renderLoggingInToken();
if (status === 'failed') return this.renderFailure();
if (status === 'ok') return this.renderLoggedIn();
return null;
}
}
IIIFAuthentication.propTypes = {
accessTokenServiceId: PropTypes.string.isRequired,
authServiceId: PropTypes.string.isRequired,
confirm: PropTypes.string,
description: PropTypes.string,
failureDescription: PropTypes.string,
failureHeader: PropTypes.string,
features: PropTypes.string,
handleAuthInteraction: PropTypes.func.isRequired,
header: PropTypes.string,
isInteractive: PropTypes.bool,
label: PropTypes.string,
logoutServiceId: PropTypes.string,
openWindow: PropTypes.func,
resetAuthenticationState: PropTypes.func.isRequired,
resolveAccessTokenRequest: PropTypes.func.isRequired,
resolveAuthenticationRequest: PropTypes.func.isRequired,
status: PropTypes.oneOf(['logout', 'ok', 'token', 'cookie', 'failed', null]),
t: PropTypes.func,
windowId: PropTypes.string.isRequired,
};
IIIFAuthentication.defaultProps = {
confirm: undefined,
description: undefined,
failureDescription: undefined,
failureHeader: undefined,
features: 'centerscreen',
header: undefined,
isInteractive: true,
label: undefined,
logoutServiceId: undefined,
openWindow: window.open,
status: null,
t: k => k,
};
......@@ -9,7 +9,7 @@ import PrimaryWindow from '../containers/PrimaryWindow';
import CompanionArea from '../containers/CompanionArea';
import MinimalWindow from '../containers/MinimalWindow';
import ErrorContent from '../containers/ErrorContent';
import WindowAuthenticationControl from '../containers/WindowAuthenticationControl';
import IIIFAuthentication from '../containers/IIIFAuthentication';
import { PluginHook } from './PluginHook';
/**
......@@ -44,7 +44,7 @@ export class Window extends Component {
windowId={windowId}
windowDraggable={windowDraggable}
/>
<WindowAuthenticationControl key="auth" windowId={windowId} />
<IIIFAuthentication windowId={windowId} />
</div>
);
if (workspaceType === 'mosaic' && windowDraggable) {
......
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
import Collapse from '@material-ui/core/Collapse';
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 { PluginHook } from './PluginHook';
/** */
export class WindowAuthenticationBar extends Component {
/** */
constructor(props) {
super(props);
this.state = { open: false };
this.setOpen = this.setOpen.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
/** */
onSubmit() {
const { onConfirm } = this.props;
this.setOpen(false);
onConfirm();
}
/** Toggle the full description */
setOpen(open) {
this.setState(state => ({ ...state, open }));
}
/** */
render() {
const {
classes, confirmButton, continueLabel,
header, description, icon, label, t,
ruleSet, hasLogoutService, status, ConfirmProps,
} = this.props;
if (status === 'ok' && !hasLogoutService) return null;
const { open } = this.state;
const button = (
<Button onClick={this.onSubmit} className={classes.buttonInvert} autoFocus color="secondary" {...ConfirmProps}>
{confirmButton || t('login')}
</Button>
);
if (!description && !header) {
return (
<Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}>
<div className={classes.topBar}>
{ icon || <LockIcon className={classes.icon} /> }
<Typography className={classes.label} component="h3" variant="body1" color="inherit">
<SanitizedHtml htmlString={label} ruleSet={ruleSet} />
</Typography>
<PluginHook {...this.props} />
{ button }
</div>
</Paper>
);
}
return (
<Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}>
<Button fullWidth className={classes.topBar} onClick={() => this.setOpen(true)} component="div" color="inherit">
{ icon || <LockIcon className={classes.icon} /> }
<Typography className={classes.label} component="h3" variant="body1" color="inherit">
<SanitizedHtml htmlString={label} ruleSet="iiif" />
</Typography>
<PluginHook {...this.props} />
<span className={classes.fauxButton}>
{ !open && (
<Typography variant="button" color="inherit">
{ continueLabel || t('continue') }
</Typography>
)}
</span>
</Button>
<Collapse
in={open}
onClose={() => this.setOpen(false)}
>
<Typography variant="body1" color="inherit" className={classes.expanded}>
<SanitizedHtml htmlString={header} ruleSet={ruleSet} />
{ header && description ? ': ' : '' }
<SanitizedHtml htmlString={description} ruleSet={ruleSet} />
</Typography>
<DialogActions>
<Button onClick={() => this.setOpen(false)} color="inherit">
{ t('cancel') }
</Button>
{ button }
</DialogActions>
</Collapse>
</Paper>
);
}
}
WindowAuthenticationBar.propTypes = {
classes: PropTypes.objectOf(PropTypes.string).isRequired,
confirmButton: PropTypes.string,
ConfirmProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types
continueLabel: PropTypes.string,
description: PropTypes.node,
hasLogoutService: PropTypes.bool,
header: PropTypes.node,
icon: PropTypes.node,
label: PropTypes.node.isRequired,
onConfirm: PropTypes.func.isRequired,
ruleSet: PropTypes.string,
status: PropTypes.string,
t: PropTypes.func,
};
WindowAuthenticationBar.defaultProps = {
confirmButton: undefined,
ConfirmProps: {},
continueLabel: undefined,
description: undefined,
hasLogoutService: true,
header: undefined,
icon: undefined,
ruleSet: 'iiif',
status: undefined,
t: k => k,
};
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
import Collapse from '@material-ui/core/Collapse';
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';
/**
*/
export class WindowAuthenticationControl extends Component {
/** */
constructor(props) {
super(props);
this.state = {
open: false,
showFailureMessage: true,
};
this.handleClickOpen = this.handleClickOpen.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleConfirm = this.handleConfirm.bind(this);
}
/** */
handleClickOpen() {
this.setState({ open: true });
}
/** */
handleClose() {
this.setState({ open: false, showFailureMessage: false });
}
/** */
handleConfirm() {
const {
handleAuthInteraction, infoId, serviceId, windowId,
} = this.props;
handleAuthInteraction(windowId, infoId, serviceId);
this.setState({ showFailureMessage: true });
}
/** */
isInteractive() {
const {
profile,
} = this.props;
return profile === 'http://iiif.io/api/auth/1/clickthrough' || profile === 'http://iiif.io/api/auth/1/login';
}
/** */
render() {
const {
classes,
confirmLabel,
degraded,
description,
failureDescription,
failureHeader,
header,
label,
profile,
status,
t,
windowId,
} = this.props;
const failed = status === 'failed';
if ((!degraded || !profile) && status !== 'fetching') return <AuthenticationLogout windowId={windowId} />;
if (!this.isInteractive() && !failed) return <></>;
const { showFailureMessage, open } = this.state;
const isInFailureState = showFailureMessage && failed;
const hasCollapsedContent = isInFailureState
? failureDescription
: header || description;
const confirmButton = (
<Button onClick={this.handleConfirm} className={classes.buttonInvert} autoFocus color="secondary">
{confirmLabel || (this.isInteractive() ? t('login') : t('retry')) }
</Button>
);
return (
<Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}>
<Button fullWidth className={classes.topBar} onClick={hasCollapsedContent ? this.handleClickOpen : this.handleConfirm} component="div" color="inherit">
<LockIcon className={classes.icon} />
<Typography className={classes.label} component="h3" variant="body1" color="inherit">
<SanitizedHtml htmlString={(isInFailureState ? failureHeader : label) || t('authenticationRequired')} ruleSet="iiif" />
</Typography>
<span className={classes.fauxButton}>
{ hasCollapsedContent
? !open && (
<Typography variant="button" color="inherit">
{ t('continue') }
</Typography>
)
: confirmButton}
</span>
</Button>
{
hasCollapsedContent && (
<Collapse
in={open}
onClose={this.handleClose}
>
<Typography variant="body1" color="inherit" className={classes.expanded}>
{
isInFailureState
? <SanitizedHtml htmlString={failureDescription || ''} ruleSet="iiif" />
: (
<>
<SanitizedHtml htmlString={header || ''} ruleSet="iiif" />
{ header && description ? ': ' : '' }
<SanitizedHtml htmlString={description || ''} ruleSet="iiif" />
</>
)
}
</Typography>
<DialogActions>
<Button onClick={this.handleClose} color="inherit">
{t('cancel')}
</Button>
{confirmButton}
</DialogActions>
</Collapse>
)
}
</Paper>
);
}
}
WindowAuthenticationControl.propTypes = {
classes: PropTypes.objectOf(PropTypes.string).isRequired,
confirmLabel: PropTypes.string,
degraded: PropTypes.bool,
description: PropTypes.string,
failureDescription: PropTypes.string,
failureHeader: PropTypes.string,
handleAuthInteraction: PropTypes.func.isRequired,
header: PropTypes.string,
infoId: PropTypes.string,
label: PropTypes.string,
profile: PropTypes.string,
serviceId: PropTypes.string,
status: PropTypes.oneOf(['ok', 'fetching', 'failed', null]),
t: PropTypes.func,
windowId: PropTypes.string.isRequired,
};
WindowAuthenticationControl.defaultProps = {
confirmLabel: undefined,
degraded: false,
description: undefined,
failureDescription: undefined,
failureHeader: undefined,
header: undefined,
infoId: undefined,
label: undefined,
profile: undefined,
serviceId: undefined,
status: null,
t: () => {},
};
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withPlugins } from '../extend/withPlugins';
import * as actions from '../state/actions';
import { getAccessTokens } from '../state/selectors';
import { AccessTokenSender } from '../components/AccessTokenSender';
/**
* mapStateToProps - to hook up connect
* @memberof App
* @private
*/
const mapStateToProps = (state) => ({
url: (Object.values(getAccessTokens(state)).find(e => e.isFetching) || {}).id,
});
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof App
* @private
*/
const mapDispatchToProps = {
handleAccessTokenMessage: actions.resolveAccessTokenRequest,
};
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withPlugins('AccessTokenSender'),
);
export default enhance(AccessTokenSender);
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);
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withPlugins } from '../extend/withPlugins';
import * as actions from '../state/actions';
import { getAuth, getConfig } from '../state/selectors';
import { AuthenticationSender } from '../components/AuthenticationSender';
/**
* mapStateToProps - to hook up connect
* @memberof App
* @private
*/
const mapStateToProps = (state) => ({
center: getConfig(state).window.authNewWindowCenter,
url: (Object.values(getAuth(state)).find(e => e.isFetching && e.profile !== 'http://iiif.io/api/auth/1/external') || {}).id,
});
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof App
* @private
*/
const mapDispatchToProps = {
handleInteraction: actions.resolveAuthenticationRequest,
};
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withPlugins('AuthenticationSender'),
);
export default enhance(AuthenticationSender);
import { connect } from 'react-redux';
import { compose } from 'redux';
import { withTranslation } from 'react-i18next';
import { Utils } from 'manifesto.js/dist-esmodule/Utils';
import { withPlugins } from '../extend/withPlugins';
import * as actions from '../state/actions';
import {
getCurrentCanvas,
getAuth,
selectCanvasAuthService,
getAccessTokens,
} from '../state/selectors';
import { IIIFAuthentication } from '../components/IIIFAuthentication';
/**
* mapStateToProps - to hook up connect
* @memberof FullScreenButton
* @private
*/
const mapStateToProps = (state, { windowId }) => {
const canvasId = (getCurrentCanvas(state, { windowId }) || {}).id;
const service = selectCanvasAuthService(state, { canvasId, windowId });
const accessTokenService = service && (
Utils.getService(service, 'http://iiif.io/api/auth/1/token')
);
const logoutService = service && (
Utils.getService(service, 'http://iiif.io/api/auth/1/logout')
);
const authStatuses = getAuth(state) || {};
const authStatus = service && authStatuses[service.id];
const accessTokens = authStatus && accessTokenService && getAccessTokens(state);
const accessTokenStatus = accessTokens && Object.values(accessTokens).find(
e => e.id === accessTokenService.id,
);
const profile = service && service.getProfile();
let status;
if (!authStatus) {
status = null;
} else if (authStatus.ok) {
status = 'ok';
} else if (authStatus.isFetching) {
if (authStatus.windowId === windowId) {
status = 'cookie';
}
} else if (accessTokenStatus && accessTokenStatus.isFetching) {
if (authStatus.windowId === windowId) {
status = 'token';
}
} else {
status = 'failed';
}
const isInteractive = profile !== 'http://iiif.io/api/auth/1/external' && profile !== 'http://iiif.io/api/auth/1/kiosk';
return {
accessTokenServiceId: accessTokenService && accessTokenService.id,
authServiceId: service && service.id,
confirm: service && service.getConfirmLabel(),
description: service && service.getDescription(),
failureDescription: service && service.getFailureDescription(),
failureHeader: service && service.getFailureHeader(),
header: service && service.getHeader(),
isInteractive,
label: service && service.getLabel()[0].value,
logoutServiceId: logoutService && logoutService.id,
profile,
status,
};
};
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof ManifestListItem
* @private
*/
const mapDispatchToProps = {
handleAuthInteraction: actions.addAuthenticationRequest,
resetAuthenticationState: actions.resetAuthenticationState,
resolveAccessTokenRequest: actions.resolveAccessTokenRequest,
resolveAuthenticationRequest: actions.resolveAuthenticationRequest,
};
const enhance = compose(
withTranslation(),
connect(mapStateToProps, mapDispatchToProps),
withPlugins('IIIFAuthentication'),
);
export default enhance(IIIFAuthentication);
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import { withStyles } from '@material-ui/core';
import { withStyles } from '@material-ui/core/styles';
import { fade } from '@material-ui/core/styles/colorManipulator';
import { withPlugins } from '../extend/withPlugins';
import * as actions from '../state/actions';
import {
getCurrentCanvas,
selectAuthStatus,
selectInfoResponse,
selectCanvasAuthService,
} from '../state/selectors';
import { WindowAuthenticationControl } from '../components/WindowAuthenticationControl';
import { WindowAuthenticationBar } from '../components/WindowAuthenticationBar';
/**
* 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 infoResponse = selectInfoResponse(state, { canvasId, windowId }) || {};
return {
confirmLabel: service && service.getConfirmLabel(),
degraded: infoResponse.degraded,
description: service && service.getDescription(),
failureDescription: service && service.getFailureDescription(),
failureHeader: service && service.getFailureHeader(),
header: service && service.getHeader(),
infoId: infoResponse.id,
label: service && service.getLabel()[0].value,
profile: service && service.getProfile(),
serviceId: service && service.id,
status: service && selectAuthStatus(state, service),
};
};
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof App
* @private
*/
const mapDispatchToProps = {
handleAuthInteraction: actions.addAuthenticationRequest,
};
/**
* @param theme
* @returns {{typographyBody: {flexGrow: number, fontSize: number|string},
......@@ -59,6 +18,9 @@ const styles = theme => ({
),
},
backgroundColor: theme.palette.secondary.contrastText,
marginLeft: theme.spacing(5),
paddingBottom: 0,
paddingTop: 0,
},
expanded: {
paddingLeft: theme.spacing(),
......@@ -86,15 +48,18 @@ const styles = theme => ({
'&:hover': {
backgroundColor: theme.palette.secondary.main,
},
alignItems: 'center',
display: 'flex',
justifyContent: 'inherit',
padding: theme.spacing(1),
textTransform: 'none',
},
});
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withStyles(styles),
withTranslation(),
withPlugins('WindowAuthenticationControl'),
withStyles(styles),
withPlugins('WindowAuthenticationBar'),
);
export default enhance(WindowAuthenticationControl);
export default enhance(WindowAuthenticationBar);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment