diff --git a/pages/accountLists.page.test.tsx b/pages/accountLists.page.test.tsx index e38dbbc80..4421ab96c 100644 --- a/pages/accountLists.page.test.tsx +++ b/pages/accountLists.page.test.tsx @@ -20,10 +20,12 @@ interface GetServerSidePropsReturn { redirect: unknown; } -const accountListId = 'accountID1'; +const accountListId = 'account-list-1'; describe('Account Lists page', () => { - const context = {} as GetServerSidePropsContext; + const context = { + resolvedUrl: '/accountLists/account-list-1', + } as GetServerSidePropsContext; describe('NextAuth unauthorized', () => { it('should redirect to login', async () => { @@ -35,7 +37,7 @@ describe('Account Lists page', () => { expect(props).toBeUndefined(); expect(redirect).toEqual({ - destination: '/login', + destination: '/login?redirect=%2FaccountLists%2Faccount-list-1', permanent: false, }); }); diff --git a/pages/accountLists/[accountListId].page.test.tsx b/pages/accountLists/[accountListId].page.test.tsx index 5a95d1fa1..7d16180b9 100644 --- a/pages/accountLists/[accountListId].page.test.tsx +++ b/pages/accountLists/[accountListId].page.test.tsx @@ -5,7 +5,6 @@ import { render } from '@testing-library/react'; import { GraphQLError } from 'graphql'; import { getSession } from 'next-auth/react'; import { I18nextProvider } from 'react-i18next'; -import { session } from '__tests__/fixtures/session'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import makeSsrClient from 'src/lib/apollo/ssrClient'; @@ -29,11 +28,12 @@ describe('AccountListsId page', () => { query: { accountListId: 'account-list-1', }, + resolvedUrl: '/accountLists/account-list-1', } as unknown as GetServerSidePropsContext; describe('NextAuth unauthorized', () => { it('should redirect to login', async () => { - (getSession as jest.Mock).mockResolvedValue(null); + (getSession as jest.Mock).mockResolvedValueOnce(null); const { props, redirect } = (await getServerSideProps( context, @@ -41,17 +41,13 @@ describe('AccountListsId page', () => { expect(props).toBeUndefined(); expect(redirect).toEqual({ - destination: '/login', + destination: '/login?redirect=%2FaccountLists%2Faccount-list-1', permanent: false, }); }); }); describe('NextAuth authorized', () => { - beforeEach(() => { - (getSession as jest.Mock).mockResolvedValue(session); - }); - it('redirects to the home page on GraphQL query error', async () => { (makeSsrClient as jest.Mock).mockReturnValue({ query: jest diff --git a/pages/account_lists/[accountListId]/accept_invite/[inviteId].page.test.tsx b/pages/account_lists/[accountListId]/accept_invite/[inviteId].page.test.tsx index fb757f5f1..c0a20f75c 100644 --- a/pages/account_lists/[accountListId]/accept_invite/[inviteId].page.test.tsx +++ b/pages/account_lists/[accountListId]/accept_invite/[inviteId].page.test.tsx @@ -1,5 +1,4 @@ import { GetServerSidePropsContext } from 'next'; -import { session } from '__tests__/fixtures/session'; import { getServerSideProps } from './[inviteId].page'; describe('Account Invite Link Redirect', () => { @@ -24,9 +23,6 @@ describe('Account Invite Link Redirect', () => { '/acceptInvite?accountListId=test-account-list-id&accountInviteId=test-invite-id&inviteCode=test-code', permanent: true, }, - props: { - session: session, - }, }); }); @@ -45,9 +41,6 @@ describe('Account Invite Link Redirect', () => { destination: '/accountLists/_/', permanent: true, }, - props: { - session: session, - }, }); }); }); diff --git a/pages/account_lists/[accountListId]/accept_invite/[inviteId].page.tsx b/pages/account_lists/[accountListId]/accept_invite/[inviteId].page.tsx index 491799a85..97bef4aac 100644 --- a/pages/account_lists/[accountListId]/accept_invite/[inviteId].page.tsx +++ b/pages/account_lists/[accountListId]/accept_invite/[inviteId].page.tsx @@ -1,32 +1,27 @@ import { GetServerSideProps } from 'next'; import { ReactNode } from 'react'; import { getSession } from 'next-auth/react'; +import { loginRedirect } from 'pages/api/utils/pagePropsHelpers'; // This page redirect old email invite links to the new page that handles invites const InvitePage = (): ReactNode => null; export const getServerSideProps: GetServerSideProps = async (context) => { const session = await getSession(context); - const { accountListId, inviteId, code } = context.query; + if (!session) { + return loginRedirect(context); + } + const { accountListId, inviteId, code } = context.query; const redirectURL = accountListId && inviteId && code ? `/acceptInvite?accountListId=${accountListId}&accountInviteId=${inviteId}&inviteCode=${code}` : // Intentionally redirect to invalid accountListId since we don't have the current user's accountListId '/accountLists/_/'; - return { - redirect: session - ? { - destination: redirectURL, - permanent: true, - } - : { - destination: '/login', - permanent: false, - }, - props: { - session, + redirect: { + destination: redirectURL, + permanent: true, }, }; }; diff --git a/pages/api/utils/pagePropsHelpers.test.ts b/pages/api/utils/pagePropsHelpers.test.ts index 87708a911..a6c1fbdaf 100644 --- a/pages/api/utils/pagePropsHelpers.test.ts +++ b/pages/api/utils/pagePropsHelpers.test.ts @@ -4,6 +4,7 @@ import { session } from '__tests__/fixtures/session'; import { enforceAdmin, loadSession, + loginRedirect, makeGetServerSideProps, } from './pagePropsHelpers'; @@ -11,8 +12,20 @@ jest.mock('next-auth/react'); const context = { query: { accountListId: 'account-list-1' }, + resolvedUrl: '/page?param=value', } as unknown as GetServerSidePropsContext; +describe('loginRedirect', () => { + it('returns redirect with current URL', () => { + expect(loginRedirect(context)).toEqual({ + redirect: { + destination: '/login?redirect=%2Fpage%3Fparam%3Dvalue', + permanent: false, + }, + }); + }); +}); + describe('enforceAdmin', () => { it('does not return a redirect if the user is an admin', async () => { (getSession as jest.Mock).mockResolvedValue({ user: { admin: true } }); @@ -50,7 +63,7 @@ describe('loadSession', () => { await expect(loadSession(context)).resolves.toMatchObject({ redirect: { - destination: '/login', + destination: '/login?redirect=%2Fpage%3Fparam%3Dvalue', }, }); }); @@ -67,7 +80,7 @@ describe('makeGetServerSideProps', () => { await expect(getServerSideProps(context)).resolves.toEqual({ redirect: { - destination: '/login', + destination: '/login?redirect=%2Fpage%3Fparam%3Dvalue', permanent: false, }, }); diff --git a/pages/api/utils/pagePropsHelpers.ts b/pages/api/utils/pagePropsHelpers.ts index de0b8fdb2..00f3e503d 100644 --- a/pages/api/utils/pagePropsHelpers.ts +++ b/pages/api/utils/pagePropsHelpers.ts @@ -10,6 +10,16 @@ interface PagePropsWithSession { session: Session; } +// Return a redirect to the login page +export const loginRedirect = ( + context: GetServerSidePropsContext, +): GetServerSidePropsResult => ({ + redirect: { + destination: `/login?redirect=${encodeURIComponent(context.resolvedUrl)}`, + permanent: false, + }, +}); + // Redirect back to the dashboard if the user isn't an admin export const enforceAdmin: GetServerSideProps = async ( context, @@ -36,13 +46,9 @@ export const loadSession: GetServerSideProps = async ( ) => { const session = await getSession(context); if (!session?.user.apiToken) { - return { - redirect: { - destination: '/login', - permanent: false, - }, - }; + return loginRedirect(context); } + return { props: { session, @@ -102,12 +108,7 @@ export const makeGetServerSideProps = >( // Start by loading the session and redirecting to the login page if it is missing const session = await getSession(context); if (!session) { - return { - redirect: { - destination: '/login', - permanent: false, - }, - }; + return loginRedirect(context); } // Pass the session to the page's custom logic to generate the page props diff --git a/pages/index.page.tsx b/pages/index.page.tsx index 5d3d7d297..631bd6bfb 100644 --- a/pages/index.page.tsx +++ b/pages/index.page.tsx @@ -2,6 +2,7 @@ import { GetServerSideProps } from 'next'; import { ReactNode } from 'react'; import { getSession } from 'next-auth/react'; import BaseLayout from 'src/components/Layouts/Basic'; +import { loginRedirect } from './api/utils/pagePropsHelpers'; const IndexPage = (): ReactNode => null; @@ -9,15 +10,15 @@ IndexPage.layout = BaseLayout; export const getServerSideProps: GetServerSideProps = async (context) => { const session = await getSession(context); + if (!session) { + return loginRedirect(context); + } return { redirect: { - destination: session ? '/accountLists' : '/login', + destination: '/accountLists', permanent: false, }, - props: { - session, - }, }; }; diff --git a/pages/login.page.tsx b/pages/login.page.tsx index 4fa97213c..800830712 100644 --- a/pages/login.page.tsx +++ b/pages/login.page.tsx @@ -11,6 +11,7 @@ import Welcome from 'src/components/Welcome'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; import { extractCookie } from 'src/lib/extractCookie'; import i18n from 'src/lib/i18n'; +import { getQueryParam } from 'src/utils/queryParam'; const SignUpBox = styled('div')(({ theme }) => ({ marginBlock: theme.spacing(2), @@ -124,10 +125,11 @@ export const getServerSideProps: GetServerSideProps = async (context) => { `mpdx-handoff.redirect-url=; HttpOnly; path=/; Max-Age=0`, ); } - if (context.res && session && !impersonateCookie) { + if (session && !impersonateCookie) { + const queryRedirectUrl = getQueryParam(context.query, 'redirect'); return { redirect: { - destination: redirectCookie ?? '/accountLists', + destination: redirectCookie ?? queryRedirectUrl ?? '/accountLists', permanent: false, }, }; diff --git a/pages/organizations/[orgId]/accept_invite/[inviteId].page.test.tsx b/pages/organizations/[orgId]/accept_invite/[inviteId].page.test.tsx index e0a3202e0..322210bb8 100644 --- a/pages/organizations/[orgId]/accept_invite/[inviteId].page.test.tsx +++ b/pages/organizations/[orgId]/accept_invite/[inviteId].page.test.tsx @@ -1,5 +1,4 @@ import { GetServerSidePropsContext } from 'next'; -import { session } from '__tests__/fixtures/session'; import { getServerSideProps } from './[inviteId].page'; describe('Org Invite Link Redirect', () => { @@ -24,9 +23,6 @@ describe('Org Invite Link Redirect', () => { '/acceptInvite?orgId=test-org-id&orgInviteId=test-invite-id&inviteCode=test-code', permanent: true, }, - props: { - session: session, - }, }); }); @@ -45,9 +41,6 @@ describe('Org Invite Link Redirect', () => { destination: '/accountLists/_/', permanent: true, }, - props: { - session: session, - }, }); }); }); diff --git a/pages/organizations/[orgId]/accept_invite/[inviteId].page.tsx b/pages/organizations/[orgId]/accept_invite/[inviteId].page.tsx index 9dd0efd11..52990722a 100644 --- a/pages/organizations/[orgId]/accept_invite/[inviteId].page.tsx +++ b/pages/organizations/[orgId]/accept_invite/[inviteId].page.tsx @@ -1,32 +1,27 @@ import { GetServerSideProps } from 'next'; import { ReactNode } from 'react'; import { getSession } from 'next-auth/react'; +import { loginRedirect } from 'pages/api/utils/pagePropsHelpers'; // This page redirect old email invite links to the new page that handles invites const OrgInvitePage = (): ReactNode => null; export const getServerSideProps: GetServerSideProps = async (context) => { const session = await getSession(context); - const { orgId, inviteId, code } = context.query; + if (!session) { + return loginRedirect(context); + } + const { orgId, inviteId, code } = context.query; const redirectURL = orgId && inviteId && code ? `/acceptInvite?orgId=${orgId}&orgInviteId=${inviteId}&inviteCode=${code}` : // Intentionally redirect to invalid accountListId since we don't have the current user's accountListId '/accountLists/_/'; - return { - redirect: session - ? { - destination: redirectURL, - permanent: true, - } - : { - destination: '/login', - permanent: false, - }, - props: { - session, + redirect: { + destination: redirectURL, + permanent: true, }, }; }; diff --git a/pages/setup/account.page.test.tsx b/pages/setup/account.page.test.tsx index a7baf8b47..7fdfe1476 100644 --- a/pages/setup/account.page.test.tsx +++ b/pages/setup/account.page.test.tsx @@ -23,7 +23,9 @@ const router = { push, }; -const context = {} as unknown as GetServerSidePropsContext; +const context = { + resolvedUrl: '/accountLists/account-list-1', +} as unknown as GetServerSidePropsContext; const mutationSpy = jest.fn(); @@ -97,7 +99,7 @@ describe('getServerSideProps', () => { await expect(getServerSideProps(context)).resolves.toEqual({ redirect: { - destination: '/login', + destination: '/login?redirect=%2FaccountLists%2Faccount-list-1', permanent: false, }, }); diff --git a/src/components/RouterGuard/RouterGuard.tsx b/src/components/RouterGuard/RouterGuard.tsx index 37623db5c..704c5b6c5 100644 --- a/src/components/RouterGuard/RouterGuard.tsx +++ b/src/components/RouterGuard/RouterGuard.tsx @@ -14,7 +14,7 @@ export const RouterGuard: React.FC = ({ children = null }) => { const session = useSession({ required: true, onUnauthenticated: () => { - push('/login'); + push({ pathname: '/login', query: { redirect: window.location.href } }); }, }); diff --git a/src/components/Settings/Admin/ImpersonateUser/ImpersonateUserAccordion.tsx b/src/components/Settings/Admin/ImpersonateUser/ImpersonateUserAccordion.tsx index d50b2d2b9..48e248fe8 100644 --- a/src/components/Settings/Admin/ImpersonateUser/ImpersonateUserAccordion.tsx +++ b/src/components/Settings/Admin/ImpersonateUser/ImpersonateUserAccordion.tsx @@ -1,3 +1,4 @@ +import { useRouter } from 'next/router'; import { ReactElement } from 'react'; import { Box, @@ -43,6 +44,7 @@ export const ImpersonateUserAccordion: React.FC = ({ const accordionName = t('Impersonate User'); const { enqueueSnackbar } = useSnackbar(); const { appName } = useGetAppSettings(); + const { push } = useRouter(); const onSubmit = async (attributes: ImpersonateUserFormType) => { try { @@ -66,7 +68,7 @@ export const ImpersonateUserAccordion: React.FC = ({ variant: 'success', }, ); - window.location.href = `/login`; + push('/login'); } else { setupImpersonateJson.errors.forEach((error) => { enqueueSnackbar(error.detail, { diff --git a/src/components/Settings/Organization/ImpersonateUser/ImpersonateUserAccordion.tsx b/src/components/Settings/Organization/ImpersonateUser/ImpersonateUserAccordion.tsx index 85e3ec14d..c7e3f55f9 100644 --- a/src/components/Settings/Organization/ImpersonateUser/ImpersonateUserAccordion.tsx +++ b/src/components/Settings/Organization/ImpersonateUser/ImpersonateUserAccordion.tsx @@ -1,3 +1,4 @@ +import { useRouter } from 'next/router'; import { ReactElement, useContext } from 'react'; import { Box, @@ -47,6 +48,7 @@ export const ImpersonateUserAccordion: React.FC = ({ const accordionName = t('Impersonate User'); const { enqueueSnackbar } = useSnackbar(); const { appName } = useGetAppSettings(); + const { push } = useRouter(); const { selectedOrganizationId } = useContext( OrganizationsContext, @@ -81,7 +83,7 @@ export const ImpersonateUserAccordion: React.FC = ({ variant: 'success', }, ); - window.location.href = `${process.env.SITE_URL}/login`; + push('/login'); } } catch (err) { enqueueSnackbar(getErrorMessage(err), { diff --git a/src/lib/apollo/client.ts b/src/lib/apollo/client.ts index 414e85549..352300359 100644 --- a/src/lib/apollo/client.ts +++ b/src/lib/apollo/client.ts @@ -30,35 +30,32 @@ const makeClient = (apiToken: string) => { link: from([ makeAuthLink(apiToken), onError(({ graphQLErrors, networkError }) => { - // Don't show sign out and display errors on the login page because the user won't be logged in - if (graphQLErrors && window.location.pathname !== '/login') { - graphQLErrors.forEach((graphQLError) => { - if (graphQLError.extensions.code === 'AUTHENTICATION_ERROR') { - signOut({ redirect: true, callbackUrl: 'signOut' }).then(() => { - clearDataDogUser(); - client.clearStore(); + graphQLErrors?.forEach((graphQLError) => { + if (graphQLError.extensions.code === 'AUTHENTICATION_ERROR') { + signOut({ redirect: true, callbackUrl: 'signOut' }).then(() => { + clearDataDogUser(); + client.clearStore(); + }); + } + if (isAccountListNotFoundError(graphQLError)) { + client + .query({ + query: GetDefaultAccountDocument, + }) + .then((response) => { + // eslint-disable-next-line no-console + console.log('Incorrect accountListId provided. Redirecting.'); + router.replace( + replaceUrlAccountList( + window.location.pathname, + response.data.user.defaultAccountList, + ), + ); }); - } - if (isAccountListNotFoundError(graphQLError)) { - client - .query({ - query: GetDefaultAccountDocument, - }) - .then((response) => { - // eslint-disable-next-line no-console - console.log('Incorrect accountListId provided. Redirecting.'); - router.replace( - replaceUrlAccountList( - window.location.pathname, - response.data.user.defaultAccountList, - ), - ); - }); - } else { - snackNotifications.error(graphQLError.message); - } - }); - } + } else { + snackNotifications.error(graphQLError.message); + } + }); if (networkError) { dispatch('mpdx-api-error');