diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e3b4f004..ebf3debd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * Idle-session timeout and "Keep working?" modal. Refs STCOR-776. * Implement password validation for Login Page. Refs STCOR-741. * Avoid deprecated `defaultProps` for functional components. Refs STCOR-844.. +* Update session data with values from `_self` request on reload. Refs STCOR-846. ## [10.1.0](https://github.com/folio-org/stripes-core/tree/v10.1.0) (2024-03-12) [Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.0.0...v10.1.0) diff --git a/src/loginServices.js b/src/loginServices.js index 543075753..1017821e5 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -390,11 +390,16 @@ function loadResources(okapiUrl, store, tenant, userId) { /** * spreadUserWithPerms - * return an object { user, perms } based on response from bl-users/self. + * Restructure the response from `bl-users/self?expandPermissions=true` + * to return an object shaped like + * { + * user: { id, username, ...personal } + * perms: { foo: true, bar: true, ... } + * } * * @param {object} userWithPerms * - * @returns {object} + * @returns {object} { user, perms } */ export function spreadUserWithPerms(userWithPerms) { const user = { @@ -403,23 +408,16 @@ export function spreadUserWithPerms(userWithPerms) { ...userWithPerms?.user?.personal, }; - // remap data's array of permission-names to set with - // permission-names for keys and `true` for values. - // - // userWithPerms is shaped differently depending on whether - // it comes from a login call or a `.../_self` call, which - // is just totally totally awesome. :| - // we'll parse it differently depending on what it looks like. - let perms = {}; + // remap userWithPerms.permissions.permissions from an array shaped like + // [{ "permissionName": "foo", ... }] + // to an object shaped like + // { foo: true, ...} + const perms = {}; const list = userWithPerms?.permissions?.permissions; if (list && Array.isArray(list) && list.length > 0) { - // _self sends data like ["foo", "bar", "bat"] - // login sends data like [{ "permissionName": "foo" }] - if (typeof list[0] === 'string') { - perms = Object.assign({}, ...list.map(p => ({ [p]: true }))); - } else { - perms = Object.assign({}, ...list.map(p => ({ [p.permissionName]: true }))); - } + list.forEach(p => { + perms[p.permissionName] = true; + }); } return { user, perms }; @@ -694,8 +692,8 @@ export function processOkapiSession(okapiUrl, store, tenant, resp, ssoToken) { * @returns {Promise} */ export function validateUser(okapiUrl, store, tenant, session) { - const { token, user, perms, tenant: sessionTenant = tenant } = session; - return fetch(`${okapiUrl}/bl-users/_self`, { + const { token, tenant: sessionTenant = tenant } = session; + return fetch(`${okapiUrl}/bl-users/_self?expandPermissions=true`, { headers: getHeaders(sessionTenant, token), credentials: 'include', mode: 'cors', @@ -706,23 +704,25 @@ export function validateUser(okapiUrl, store, tenant, session) { store.dispatch(setAuthError(null)); store.dispatch(setLoginData(data)); - // If the request succeeded, we know the AT must be valid, but the - // response body from this endpoint doesn't include token-expiration - // data. So ... we set a near-future RT and an already-expired AT. - // On the next request, the expired AT will prompt an RTR cycle and - // we'll get real expiration values then. - const tokenExpiration = { - atExpires: -1, - rtExpires: Date.now() + (10 * 60 * 1000), - }; - + const { user, perms } = spreadUserWithPerms(data); + store.dispatch(setCurrentPerms(perms)); + + // update the session data with values from the response from _self + // in case they have changed since the previous login. this allows + // permissions changes to take effect immediately, without needing to + // re-authenticate. + // + // tenant and tokenExpiration data are still pulled from the session, + // tenant because the user may have switched the session-tenant to + // something other than their default and tokenExpiration because that + // data isn't provided by _self. store.dispatch(setSessionData({ isAuthenticated: true, user: data.user, perms, tenant: sessionTenant, token, - tokenExpiration, + tokenExpiration: session.tokenExpiration })); return loadResources(okapiUrl, store, sessionTenant, user.id); diff --git a/src/loginServices.test.js b/src/loginServices.test.js index 7e185c2ea..bec0d559f 100644 --- a/src/loginServices.test.js +++ b/src/loginServices.test.js @@ -34,7 +34,7 @@ import { setIsAuthenticated, setOkapiReady, setServerDown, - // setSessionData, + setSessionData, // setTokenExpiration, setLoginData, updateCurrentUser, @@ -311,6 +311,50 @@ describe('validateUser', () => { mockFetchCleanUp(); }); + it('overwrites session data with new values from _self', async () => { + const store = { + dispatch: jest.fn(), + }; + + const tenant = 'tenant'; + const sessionTenant = 'sessionTenant'; + const data = { + user: { + id: 'ego', + username: 'superego', + }, + permissions: { + permissions: [{ permissionName: 'ask' }, { permissionName: 'tell' }], + } + }; + + const session = { + user: { id: 'id', username: 'username' }, + perms: { foo: true }, + tenant: sessionTenant, + token: 'token', + }; + + mockFetchSuccess(data); + + await validateUser('url', store, tenant, session); + + const updatedSession = { + user: data.user, + isAuthenticated: true, + perms: { ask: true, tell: true }, + tenant: session.tenant, + token: session.token, + }; + + expect(store.dispatch).toHaveBeenNthCalledWith(1, setAuthError(null)); + expect(store.dispatch).toHaveBeenNthCalledWith(2, setLoginData(data)); + expect(store.dispatch).toHaveBeenNthCalledWith(3, setCurrentPerms({ ask: true, tell: true })); + expect(store.dispatch).toHaveBeenNthCalledWith(4, setSessionData(updatedSession)); + + mockFetchCleanUp(); + }); + it('handles invalid user', async () => { const store = { dispatch: jest.fn(),