Skip to content

Commit

Permalink
Auth2 Probe responses WIP - needs specs for:
Browse files Browse the repository at this point in the history
- src/state/sagas/auth.js
- src/state/sagas/iiif.js
- src/state/sagas/windows.js
- src/state/selectors/auth.js
- src/state/selectors/canvases.js
  • Loading branch information
barmintor committed Oct 25, 2023
1 parent bc166cb commit 0231d8e
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 29 deletions.
57 changes: 55 additions & 2 deletions src/state/sagas/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Utils } from 'manifesto.js';
import flatten from 'lodash/flatten';
import ActionTypes from '../actions/action-types';
import MiradorCanvas from '../../lib/MiradorCanvas';
import { getTokenService } from '../../lib/getServices';
import { getTokenService, getProbeService } from '../../lib/getServices';
import {
addAuthenticationRequest,
resolveAuthenticationRequest,
Expand All @@ -14,13 +14,14 @@ import {
} from '../actions';
import {
selectInfoResponses,
selectProbeResponses,
getVisibleCanvases,
getWindows,
getConfig,
getAuth,
getAccessTokens,
} from '../selectors';
import { fetchInfoResponse } from './iiif';
import { fetchInfoResponse, fetchProbeResponse } from './iiif';

/** */
export function* refetchInfoResponsesOnLogout({ tokenServiceId }) {
Expand Down Expand Up @@ -70,6 +71,54 @@ export function* refetchInfoResponses({ serviceId }) {
}));
}

/** */
export function* refetchProbeResponsesOnLogout({ tokenServiceId }) {
// delay logout actions to give the cookie service a chance to invalidate our cookies
// before we reinitialize openseadragon and rerequest images.

yield delay(2000);
yield call(refetchProbeResponses, { serviceId: tokenServiceId });
}

/**
* Figure out what probe responses could have used the access token service and:
* - refetch, if they are currently visible
* - throw them out (and lazy re-fetch) otherwise
*/
export function* refetchProbeResponses({ serviceId }) {
const windows = yield select(getWindows);

const canvases = yield all(
Object.keys(windows).map(windowId => select(getVisibleCanvases, { windowId })),
);

const visibleProbeServiceIds = flatten(flatten(canvases).map((canvas) => {
const miradorCanvas = new MiradorCanvas(canvas);
return miradorCanvas.imageResources.filter((r) => getProbeService(r)).map((r) => getProbeService(r));
}));

const probeResponses = yield select(selectProbeResponses);
/** */
const haveThisTokenService = probeResponse => {
const services = Utils.getServices(probeResponse);
return services.some(e => {
const probeTokenService = getTokenService(e);
return probeTokenService && probeTokenService.id === serviceId;
});
};

const obsoleteProbeResponses = Object.values(probeResponses).filter(
i => i.json && haveThisTokenService(i.json),
);

yield all(obsoleteProbeResponses.map(({ id: probeId }) => {
if (visibleProbeServiceIds.includes(probeId)) {
return call(fetchProbeResponse, { probeId });
}
return put({ probeId, type: ActionTypes.REMOVE_PROBE_RESPONSE });
}));
}

/** try to start any non-interactive auth flows */
export function* doAuthWorkflow({ infoJson, windowId }) {
const auths = yield select(getAuth);
Expand Down Expand Up @@ -152,9 +201,13 @@ export function* invalidateInvalidAuth({ serviceId }) {
export default function* authSaga() {
yield all([
takeEvery(ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE, rerequestOnAccessTokenFailure),
takeEvery(ActionTypes.RECEIVE_DEGRADED_PROBE_RESPONSE, rerequestOnAccessTokenFailure),
takeEvery(ActionTypes.RECEIVE_ACCESS_TOKEN_FAILURE, invalidateInvalidAuth),
takeEvery(ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE, doAuthWorkflow),
takeEvery(ActionTypes.RECEIVE_DEGRADED_PROBE_RESPONSE, doAuthWorkflow),
takeEvery(ActionTypes.RECEIVE_ACCESS_TOKEN, refetchInfoResponses),
takeEvery(ActionTypes.RECEIVE_ACCESS_TOKEN, refetchProbeResponses),
takeEvery(ActionTypes.RESET_AUTHENTICATION_STATE, refetchInfoResponsesOnLogout),
takeEvery(ActionTypes.RESET_AUTHENTICATION_STATE, refetchProbeResponsesOnLogout),
]);
}
42 changes: 36 additions & 6 deletions src/state/sagas/iiif.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import {
all, call, put, select, takeEvery,
} from 'redux-saga/effects';
import { Utils } from 'manifesto.js';
import normalizeUrl from 'normalize-url';
import ActionTypes from '../actions/action-types';
import {
receiveManifest, receiveManifestFailure, receiveInfoResponse,
receiveInfoResponseFailure, receiveDegradedInfoResponse,
receiveSearch, receiveSearchFailure,
receiveAnnotation, receiveAnnotationFailure,
receiveProbeResponse, receiveProbeResponseFailure,
receiveDegradedProbeResponse,
} from '../actions';
import { getTokenService } from '../../lib/getServices';
import { anyAuthServices, getTokenService } from '../../lib/getServices';
import {
getManifests,
getRequestsConfig,
getAccessTokens,
selectInfoResponse,
selectProbeResponse,
} from '../selectors';

/** */
Expand Down Expand Up @@ -80,10 +82,13 @@ function* fetchIiifResourceWithAuth(url, iiifResource, options, { degraded, fail

const id = json['@id'] || json.id;
if (response.ok) {
if (normalizeUrl(id, { stripAuthentication: false })
if (id && normalizeUrl(id, { stripAuthentication: false })
=== normalizeUrl(url.replace(/info\.json$/, ''), { stripAuthentication: false })) {
yield put(success({ json, response, tokenServiceId }));
return;
if (!json.substitute) {
// substitute indicates the Auth2 equivalent of a degraded response, should fall through
yield put(success({ json, response, tokenServiceId }));
return;
}
}
} else if (response.status !== 401) {
yield put(failure({
Expand Down Expand Up @@ -121,7 +126,7 @@ function* getAccessTokenService(resource) {
const manifestoCompatibleResource = resource && resource.__jsonld
? resource
: { ...resource, options: {} };
const services = Utils.getServices(manifestoCompatibleResource).filter(s => s.getProfile().match(/http:\/\/iiif.io\/api\/auth\//));
const services = anyAuthServices(manifestoCompatibleResource);
if (services.length === 0) return undefined;

const accessTokens = yield select(getAccessTokens);
Expand Down Expand Up @@ -161,6 +166,30 @@ export function* fetchInfoResponse({ imageResource, infoId, windowId }) {
yield call(fetchIiifResourceWithAuth, `${infoId.replace(/\/$/, '')}/info.json`, iiifResource, {}, callbacks);
}

/** @private */
export function* fetchProbeResponse({ resource, probeId, windowId }) {
let iiifResource = resource;
if (!iiifResource) {
iiifResource = yield select(selectProbeResponse, { probeId });
}

const callbacks = {
degraded: ({
json, response, tokenServiceId,
}) => receiveDegradedProbeResponse(probeId, json, response.ok, tokenServiceId, windowId),
failure: ({
error, json, response, tokenServiceId,
}) => (
receiveProbeResponseFailure(probeId, error, tokenServiceId)
),
success: ({
json, response, tokenServiceId,
}) => receiveProbeResponse(probeId, json, response.ok, tokenServiceId),
};

yield call(fetchIiifResourceWithAuth, probeId, iiifResource, {}, callbacks);
}

/** @private */
export function* fetchSearchResponse({
windowId, companionWindowId, query, searchId,
Expand Down Expand Up @@ -215,6 +244,7 @@ export default function* iiifSaga() {
yield all([
takeEvery(ActionTypes.REQUEST_MANIFEST, fetchManifest),
takeEvery(ActionTypes.REQUEST_INFO_RESPONSE, fetchInfoResponse),
takeEvery(ActionTypes.REQUEST_PROBE_RESPONSE, fetchProbeResponse),
takeEvery(ActionTypes.REQUEST_SEARCH, fetchSearchResponse),
takeEvery(ActionTypes.REQUEST_ANNOTATION, fetchAnnotation),
takeEvery(ActionTypes.ADD_RESOURCE, fetchResourceManifest),
Expand Down
19 changes: 19 additions & 0 deletions src/state/sagas/windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
import ActionTypes from '../actions/action-types';
import MiradorManifest from '../../lib/MiradorManifest';
import MiradorCanvas from '../../lib/MiradorCanvas';
import { getProbeService } from '../../lib/getServices';
import {
setContentSearchCurrentAnnotation,
selectAnnotation,
Expand All @@ -13,6 +14,7 @@ import {
fetchSearch,
receiveManifest,
fetchInfoResponse,
fetchProbeResponse,
showCollectionDialog,
} from '../actions';
import {
Expand All @@ -27,6 +29,7 @@ import {
getElasticLayout,
getCanvases,
selectInfoResponses,
selectProbeResponses,
getWindowConfig,
} from '../selectors';
import { fetchManifests } from './iiif';
Expand Down Expand Up @@ -250,6 +253,21 @@ export function* fetchInfoResponses({ visibleCanvases: visibleCanvasIds, windowI
}));
}

/** Fetch probe responses for the visible canvases */
export function* fetchProbeResponses({ visibleCanvases: visibleCanvasIds, windowId }) {
const canvases = yield select(getCanvases, { windowId });
const probeResponses = yield select(selectProbeResponses);
const visibleCanvases = (canvases || []).filter(c => visibleCanvasIds.includes(c.id));

yield all(visibleCanvases.map((canvas) => {
const miradorCanvas = new MiradorCanvas(canvas);
return all(miradorCanvas.imageResources.filter((r) => getProbeService(r)).map(resource => (
!probeResponses[getProbeService(resource).id]
&& put(fetchProbeResponse({ resource, windowId }))
)).filter(Boolean));
}));
}

/** */
export function* determineAndShowCollectionDialog(manifestId, windowId) {
const manifestoInstance = yield select(getManifestoInstance, { manifestId });
Expand All @@ -266,6 +284,7 @@ export default function* windowsSaga() {
takeEvery(ActionTypes.UPDATE_WINDOW, setCanvasOnNewSequence),
takeEvery(ActionTypes.SET_CANVAS, setCurrentAnnotationsOnCurrentCanvas),
takeEvery(ActionTypes.SET_CANVAS, fetchInfoResponses),
takeEvery(ActionTypes.SET_CANVAS, fetchProbeResponses),
takeEvery(ActionTypes.UPDATE_COMPANION_WINDOW, fetchCollectionManifests),
takeEvery(ActionTypes.SET_WINDOW_VIEW_TYPE, updateVisibleCanvases),
takeEvery(ActionTypes.RECEIVE_SEARCH, setCanvasOfFirstSearchResult),
Expand Down
25 changes: 9 additions & 16 deletions src/state/selectors/auth.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { createSelector } from 'reselect';
import { Utils } from 'manifesto.js';
import flatten from 'lodash/flatten';
import { anyProbeServices } from '../../lib/getServices';
import {
audioResourcesFrom, filterByTypes, textResourcesFrom, videoResourcesFrom,
audioResourcesFrom, iiifImageResourcesFrom, textResourcesFrom, videoResourcesFrom,
} from '../../lib/typeFilters';
import MiradorCanvas from '../../lib/MiradorCanvas';
import { miradorSlice } from './utils';
import { getConfig } from './config';
import { getVisibleCanvases, selectInfoResponses } from './canvases';
import { getVisibleCanvases, selectInfoResponses, selectProbeResponses } from './canvases';

export const getAuthProfiles = createSelector(
[
Expand All @@ -26,19 +27,19 @@ export const selectCurrentAuthServices = createSelector(
[
getVisibleCanvases,
selectInfoResponses,
selectProbeResponses,
getAuthProfiles,
getAuth,
(state, { iiifResources }) => iiifResources,
],
(canvases, infoResponses = {}, serviceProfiles, auth, iiifResources) => {
(canvases, infoResponses = {}, probeResponses = {}, serviceProfiles, auth, iiifResources) => {
let currentAuthResources = iiifResources;

if (!currentAuthResources && canvases) {
currentAuthResources = flatten(canvases.map(c => {
const miradorCanvas = new MiradorCanvas(c);
const images = miradorCanvas.iiifImageResources;

return images.map(i => {
const canvasResources = miradorCanvas.imageResources;
const authResources = iiifImageResourcesFrom(canvasResources).map(i => {
const iiifImageService = i.getServices()[0];

const infoResponse = infoResponses[iiifImageService.id];
Expand All @@ -48,14 +49,7 @@ export const selectCurrentAuthServices = createSelector(

return iiifImageService;
});
}));
}

if (currentAuthResources.length === 0 && canvases) {
currentAuthResources = flatten(canvases.map(c => {
const miradorCanvas = new MiradorCanvas(c);
const canvasResources = miradorCanvas.imageResources;
return videoResourcesFrom(canvasResources)
return authResources.concat(videoResourcesFrom(canvasResources))
.concat(audioResourcesFrom(canvasResources))
.concat(textResourcesFrom(canvasResources));
}));
Expand All @@ -67,7 +61,7 @@ export const selectCurrentAuthServices = createSelector(
const currentAuthServices = currentAuthResources.map(resource => {
let lastAttemptedService;
const resourceServices = Utils.getServices(resource);
const probeServices = filterByTypes(resourceServices, 'AuthProbeService2');
const probeServices = anyProbeServices(resource);
const probeServiceServices = flatten(probeServices.map(p => Utils.getServices(p)));

for (const authProfile of serviceProfiles) {
Expand All @@ -91,7 +85,6 @@ export const selectCurrentAuthServices = createSelector(
if (service && !h[service.id]) {
h[service.id] = service; // eslint-disable-line no-param-reassign
}

return h;
}, {}));
},
Expand Down
Loading

0 comments on commit 0231d8e

Please sign in to comment.