From 780dd037627d07d8f098f71cc080389d2ad53632 Mon Sep 17 00:00:00 2001 From: Marko Haarni Date: Wed, 20 Sep 2023 17:28:32 +0300 Subject: [PATCH] HAI-1311 Redirect user to original route after login (#372) If user tries to access page that requires login, that path is saved to session storage and user is redirected to login and after successful login user is redirected to the path in session storage. --- package.json | 1 - src/common/routes/AppRoutes.tsx | 7 +- src/common/routes/PrivateRoute.tsx | 21 ++++- src/common/routes/Routes.test.tsx | 79 +++++++++++++++++++ src/common/routes/constants.ts | 1 + src/domain/auth/authService.test.ts | 3 - .../accessRights/AccessRightsView.test.tsx | 2 +- src/setupTests.ts | 1 - yarn.lock | 5 -- 9 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 src/common/routes/Routes.test.tsx create mode 100644 src/common/routes/constants.ts diff --git a/package.json b/package.json index 77affd3f9..d90f051d6 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "i18next-browser-languagedetector": "^7.0.1", "immer": "8.0.2", "jest-fetch-mock": "^3.0.3", - "jest-localstorage-mock": "^2.4.6", "local-web-server": "^4.2.1", "lodash": "^4.17.21", "msw": "^0.49.0", diff --git a/src/common/routes/AppRoutes.tsx b/src/common/routes/AppRoutes.tsx index 9a24cb9d1..6083e4f78 100644 --- a/src/common/routes/AppRoutes.tsx +++ b/src/common/routes/AppRoutes.tsx @@ -6,9 +6,11 @@ import Login from '../../domain/auth/components/Login'; import OidcCallback from '../../domain/auth/components/OidcCallback'; import { LOGIN_CALLBACK_PATH, LOGIN_PATH } from '../../domain/auth/constants'; import LocaleRoutes from './LocaleRoutes'; +import { REDIRECT_PATH_KEY } from './constants'; const AppRoutes: React.FC> = () => { const currentLocale = useLocale(); + const redirectPath = sessionStorage.getItem(REDIRECT_PATH_KEY); return ( @@ -17,7 +19,10 @@ const AppRoutes: React.FC> = () => { {Object.values(LANGUAGES).map((locale) => ( } key={locale} /> ))} - } /> + } + /> ); }; diff --git a/src/common/routes/PrivateRoute.tsx b/src/common/routes/PrivateRoute.tsx index ac2838aa0..21220acac 100644 --- a/src/common/routes/PrivateRoute.tsx +++ b/src/common/routes/PrivateRoute.tsx @@ -1,6 +1,7 @@ -import React from 'react'; -import { Navigate } from 'react-router-dom'; +import React, { useEffect } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; import useUser from '../../domain/auth/useUser'; +import { REDIRECT_PATH_KEY } from './constants'; type Props = { element: JSX.Element; @@ -8,9 +9,21 @@ type Props = { const PrivateRoute: React.FC> = ({ element }) => { const { data: user } = useUser(); + const isAuthenticated = Boolean(user?.profile); + const location = useLocation(); - if (!user?.profile) { - return ; + useEffect(() => { + if (isAuthenticated) { + sessionStorage.removeItem(REDIRECT_PATH_KEY); + } + }, [isAuthenticated]); + + if (!isAuthenticated) { + // If user tries to access private route when not signed in, + // save URL path to session storage and navigate to login, + // so that user can be redirected to that route after login + sessionStorage.setItem(REDIRECT_PATH_KEY, location.pathname); + return ; } return element; diff --git a/src/common/routes/Routes.test.tsx b/src/common/routes/Routes.test.tsx new file mode 100644 index 000000000..be666dace --- /dev/null +++ b/src/common/routes/Routes.test.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { I18nextProvider } from 'react-i18next'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { InitialEntry } from '@remix-run/router'; +import { User } from 'oidc-client'; +import { Provider } from 'react-redux'; +import AppRoutes from './AppRoutes'; +import { FeatureFlagsProvider } from '../components/featureFlags/FeatureFlagsContext'; +import { GlobalNotificationProvider } from '../components/globalNotification/GlobalNotificationContext'; +import authService from '../../domain/auth/authService'; +import { store } from '../redux/store'; +import i18n from '../../locales/i18n'; +import { REDIRECT_PATH_KEY } from './constants'; + +afterEach(() => { + sessionStorage.clear(); +}); + +const path = '/fi/johtoselvityshakemus'; + +const mockUser: Partial = { + id_token: 'fffff-aaaaaa-11111', + access_token: '.BnutWVN1x7RSAP5bU2a-tXdVPuof_9pBNd_Ozw', + profile: { + iss: '', + sub: '', + aud: '', + exp: 0, + iat: 0, + }, +}; + +function getWrapper(routerInitialEntries?: InitialEntry[]) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retryDelay: 0, + }, + }, + }); + + return render( + + + + + + + + + + + + + , + ); +} + +test('Should save path to session storage and navigate to login if trying to navigate to private route', async () => { + const login = jest.spyOn(authService, 'login').mockResolvedValue(); + + getWrapper([path]); + + await waitFor(() => expect(login).toHaveBeenCalled()); + expect(window.sessionStorage.getItem(REDIRECT_PATH_KEY)).toBe(path); +}); + +test('Should redirect to path stored in session storage after login if one exists', async () => { + sessionStorage.setItem(REDIRECT_PATH_KEY, path); + jest.spyOn(authService.userManager, 'getUser').mockResolvedValue(mockUser as User); + + getWrapper(); + + await waitFor(() => + expect(window.document.title).toBe('Haitaton - Luo uusi johtoselvityshakemus'), + ); +}); diff --git a/src/common/routes/constants.ts b/src/common/routes/constants.ts new file mode 100644 index 000000000..fe40af578 --- /dev/null +++ b/src/common/routes/constants.ts @@ -0,0 +1 @@ +export const REDIRECT_PATH_KEY = 'redirect-path'; diff --git a/src/domain/auth/authService.test.ts b/src/domain/auth/authService.test.ts index 500014f2e..25d0de0d9 100644 --- a/src/domain/auth/authService.test.ts +++ b/src/domain/auth/authService.test.ts @@ -12,9 +12,6 @@ describe('authService', () => { afterEach(() => { localStorage.clear(); - // eslint-disable-next-line - // @ts-ignore - localStorage.setItem.mockClear(); jest.restoreAllMocks(); }); diff --git a/src/domain/hanke/accessRights/AccessRightsView.test.tsx b/src/domain/hanke/accessRights/AccessRightsView.test.tsx index 6063d1ad0..f9c44ddd7 100644 --- a/src/domain/hanke/accessRights/AccessRightsView.test.tsx +++ b/src/domain/hanke/accessRights/AccessRightsView.test.tsx @@ -7,7 +7,7 @@ import { server } from '../../mocks/test-server'; import usersData from '../../mocks/data/users-data.json'; import { SignedInUser } from '../hankeUsers/hankeUser'; -jest.setTimeout(30000); +jest.setTimeout(40000); afterEach(cleanup); diff --git a/src/setupTests.ts b/src/setupTests.ts index 40f91c1d7..81b85af06 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -4,7 +4,6 @@ // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; import '@testing-library/jest-dom/extend-expect'; -import 'jest-localstorage-mock'; import { GlobalWithFetchMock } from 'jest-fetch-mock'; import 'jest-canvas-mock'; import { server } from './domain/mocks/test-server'; diff --git a/yarn.lock b/yarn.lock index f2698708b..cdf967e3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10787,11 +10787,6 @@ jest-leak-detector@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" -jest-localstorage-mock@^2.4.6: - version "2.4.6" - resolved "https://registry.yarnpkg.com/jest-localstorage-mock/-/jest-localstorage-mock-2.4.6.tgz#ebb9481943bf52e0f8c864ae4858eb791d68149d" - integrity sha512-8n6tuqscEShpvC7vkq3BPabOGGszD1cdLAKTtTtCRqH2bWJIVfpV4aIhrDJstV7xLOjo56wSVZkoMbT7dWLIVg== - jest-matcher-utils@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab"