diff --git a/packages/adapter-nextjs/__tests__/auth/createAuthRouteHandlersFactory.test.ts b/packages/adapter-nextjs/__tests__/auth/createAuthRouteHandlersFactory.test.ts index c97d0d964a1..5f96321ff95 100644 --- a/packages/adapter-nextjs/__tests__/auth/createAuthRouteHandlersFactory.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/createAuthRouteHandlersFactory.test.ts @@ -62,6 +62,8 @@ const mockIsNextRequest = jest.mocked(isNextRequest); const mockIsAuthRoutesHandlersContext = jest.mocked( isAuthRoutesHandlersContext, ); +const mockRunWithAmplifyServerContext = + jest.fn() as jest.MockedFunction; describe('createAuthRoutesHandlersFactory', () => { const existingProcessEnvVars = { ...process.env }; @@ -84,6 +86,7 @@ describe('createAuthRoutesHandlersFactory', () => { createAuthRouteHandlersFactory({ config: mockAmplifyConfig, runtimeOptions: mockRuntimeOptions, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }), ).toThrow('Could not find the AMPLIFY_APP_ORIGIN environment variable.'); }); @@ -92,6 +95,7 @@ describe('createAuthRoutesHandlersFactory', () => { createAuthRouteHandlersFactory({ config: mockAmplifyConfig, runtimeOptions: mockRuntimeOptions, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockAssertTokenProviderConfig).toHaveBeenCalledWith( @@ -106,6 +110,7 @@ describe('createAuthRoutesHandlersFactory', () => { const testCreateAuthRoutesHandlersFactoryInput = { config: mockAmplifyConfig, runtimeOptions: mockRuntimeOptions, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }; const testCreateAuthRoutesHandlersInput: CreateAuthRoutesHandlersInput = { customState: 'random-state', @@ -151,6 +156,7 @@ describe('createAuthRoutesHandlersFactory', () => { setCookieOptions: mockRuntimeOptions.cookies, origin: 'https://example.com', userPoolClientId: 'def', + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); }); @@ -172,6 +178,7 @@ describe('createAuthRoutesHandlersFactory', () => { setCookieOptions: mockRuntimeOptions.cookies, origin: 'https://example.com', userPoolClientId: 'def', + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); }); @@ -192,6 +199,7 @@ describe('createAuthRoutesHandlersFactory', () => { const createAuthRoutesHandlers = createAuthRouteHandlersFactory({ config: mockAmplifyConfig, runtimeOptions: undefined, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); const handlerWithDefaultParamValues = createAuthRoutesHandlers(/* undefined */); @@ -214,6 +222,7 @@ describe('createAuthRoutesHandlersFactory', () => { setCookieOptions: {}, origin: 'https://example.com', userPoolClientId: 'def', + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); }); }); diff --git a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts index 58fdb69e5e4..a21143a965b 100644 --- a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts @@ -12,8 +12,14 @@ import { handleSignOutCallbackRequest, handleSignOutRequest, } from '../../src/auth/handlers'; +import { NextServer } from '../../src'; +import { + hasUserSignedInWithAppRouter, + isSupportedAuthApiRoutePath, +} from '../../src/auth/utils'; jest.mock('../../src/auth/handlers'); +jest.mock('../../src/auth/utils'); const mockHandleSignInSignUpRequest = jest.mocked(handleSignInSignUpRequest); const mockHandleSignOutRequest = jest.mocked(handleSignOutRequest); @@ -23,6 +29,14 @@ const mockHandleSignInCallbackRequest = jest.mocked( const mockHandleSignOutCallbackRequest = jest.mocked( handleSignOutCallbackRequest, ); +const mockHasUserSignedInWithAppRouter = jest.mocked( + hasUserSignedInWithAppRouter, +); +const mockIsSupportedAuthApiRoutePath = jest.mocked( + isSupportedAuthApiRoutePath, +); +const mockRunWithAmplifyServerContext = + jest.fn() as jest.MockedFunction; describe('handleAuthApiRouteRequestForAppRouter', () => { const testOrigin = 'https://example.com'; @@ -40,6 +54,11 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { }; const _ = handleAuthApiRouteRequestForAppRouter; + beforeAll(() => { + mockHasUserSignedInWithAppRouter.mockResolvedValue(false); + mockIsSupportedAuthApiRoutePath.mockReturnValue(true); + }); + it('returns a 405 response when input.request has an unsupported method', async () => { const request = new NextRequest( new URL('https://example.com/api/auth/sign-in'), @@ -55,6 +74,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response.status).toBe(405); @@ -75,6 +95,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response.status).toBe(400); @@ -87,6 +108,9 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { method: 'GET', }, ); + + mockIsSupportedAuthApiRoutePath.mockReturnValueOnce(false); + const response = await handleAuthApiRouteRequestForAppRouter({ request, handlerContext: { params: { slug: 'exchange-token' } }, @@ -95,6 +119,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response.status).toBe(404); @@ -124,6 +149,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response).toBe(mockResponse); @@ -139,6 +165,36 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { }, ); + test.each([['sign-in'], ['sign-up']])( + `calls hasUserSignedInWithAppRouter with correct params when handlerContext.params.slug is %s, and when it returns true, the handler returns a 302 response`, + async slug => { + mockHasUserSignedInWithAppRouter.mockResolvedValueOnce(true); + const mockRequest = new NextRequest( + new URL('https://example.com/api/auth/sign-in'), + { + method: 'GET', + }, + ); + + const response = await handleAuthApiRouteRequestForAppRouter({ + request: mockRequest, + handlerContext: { params: { slug } }, + handlerInput: { + ...testHandlerInput, + redirectOnSignInComplete: undefined, + }, + userPoolClientId: 'userPoolClientId', + oAuthConfig: testOAuthConfig, + setCookieOptions: {}, + origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(response.status).toBe(302); + expect(response.headers.get('Location')).toBe('/'); + }, + ); + it('calls handleSignOutRequest with correct params when handlerContext.params.slug is sign-out', async () => { const mockRequest = new NextRequest( new URL('https://example.com/api/auth/sign-out'), @@ -158,6 +214,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response).toBe(mockResponse); @@ -188,6 +245,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response).toBe(mockResponse); @@ -220,6 +278,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response).toBe(mockResponse); diff --git a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts index 3561c8e8153..1e6603f71f5 100644 --- a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts @@ -9,10 +9,16 @@ import { handleSignOutCallbackRequestForPagesRouter, handleSignOutRequestForPagesRouter, } from '../../src/auth/handlers'; +import { NextServer } from '../../src'; +import { + hasUserSignedInWithPagesRouter, + isSupportedAuthApiRoutePath, +} from '../../src/auth/utils'; import { createMockNextApiResponse } from './testUtils'; jest.mock('../../src/auth/handlers'); +jest.mock('../../src/auth/utils'); const mockHandleSignInSignUpRequestForPagesRouter = jest.mocked( handleSignInSignUpRequestForPagesRouter, @@ -26,6 +32,14 @@ const mockHandleSignInCallbackRequestForPagesRouter = jest.mocked( const mockHandleSignOutCallbackRequestForPagesRouter = jest.mocked( handleSignOutCallbackRequestForPagesRouter, ); +const mockIsSupportedAuthApiRoutePath = jest.mocked( + isSupportedAuthApiRoutePath, +); +const mockHasUserSignedInWithPagesRouter = jest.mocked( + hasUserSignedInWithPagesRouter, +); +const mockRunWithAmplifyServerContext = + jest.fn() as jest.MockedFunction; describe('handleAuthApiRouteRequestForPagesRouter', () => { const testOrigin = 'https://example.com'; @@ -50,6 +64,11 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { mockResponse, } = createMockNextApiResponse(); + beforeAll(() => { + mockHasUserSignedInWithPagesRouter.mockResolvedValue(false); + mockIsSupportedAuthApiRoutePath.mockReturnValue(true); + }); + afterEach(() => { mockResponseAppendHeader.mockClear(); mockResponseEnd.mockClear(); @@ -69,6 +88,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: testSetCookieOptions, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockResponseStatus).toHaveBeenCalledWith(405); @@ -86,6 +106,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: testSetCookieOptions, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockResponseStatus).toHaveBeenCalledWith(400); @@ -98,6 +119,8 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { query: { slug: 'exchange-token' }, } as any; + mockIsSupportedAuthApiRoutePath.mockReturnValueOnce(false); + handleAuthApiRouteRequestForPagesRouter({ request: mockRequest, response: mockResponse, @@ -106,6 +129,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: testSetCookieOptions, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockResponseStatus).toHaveBeenCalledWith(404); @@ -132,6 +156,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockHandleSignInSignUpRequestForPagesRouter).toHaveBeenCalledWith({ @@ -147,6 +172,34 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { }, ); + test.each([['sign-in'], ['sign-up']])( + `calls hasUserSignedInWithPagesRouter with correct params when handlerContext.params.slug is %s, and when it returns true, the handler returns a 302 response`, + async slug => { + mockHasUserSignedInWithPagesRouter.mockResolvedValueOnce(true); + const mockRequest = { + url: 'https://example.com/api/auth/sign-in', + method: 'GET', + query: { slug }, + } as unknown as NextApiRequest; + + await handleAuthApiRouteRequestForPagesRouter({ + request: mockRequest, + response: mockResponse, + handlerInput: { + ...testHandlerInput, + redirectOnSignInComplete: undefined, + }, + userPoolClientId: 'userPoolClientId', + oAuthConfig: testOAuthConfig, + setCookieOptions: {}, + origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(mockResponseRedirect).toHaveBeenCalledWith(302, '/'); + }, + ); + it('calls handleSignOutRequest with correct params when handlerContext.params.slug is sign-out', async () => { const mockRequest = { url: 'https://example.com/api/auth/sign-in', @@ -162,6 +215,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockHandleSignOutRequestForPagesRouter).toHaveBeenCalledWith({ @@ -188,6 +242,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockHandleSignInCallbackRequestForPagesRouter).toHaveBeenCalledWith({ @@ -216,6 +271,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockHandleSignOutCallbackRequestForPagesRouter).toHaveBeenCalledWith( diff --git a/packages/adapter-nextjs/__tests__/auth/utils/hasUserSignedIn.test.ts b/packages/adapter-nextjs/__tests__/auth/utils/hasUserSignedIn.test.ts new file mode 100644 index 00000000000..e1e6dd32575 --- /dev/null +++ b/packages/adapter-nextjs/__tests__/auth/utils/hasUserSignedIn.test.ts @@ -0,0 +1,128 @@ +/** + * @jest-environment node + */ +import { getCurrentUser } from 'aws-amplify/auth/server'; +import { NextRequest } from 'next/server'; +import { AuthUser } from '@aws-amplify/auth/cognito'; +import { NextApiRequest } from 'next'; + +import { + hasUserSignedInWithAppRouter, + hasUserSignedInWithPagesRouter, +} from '../../../src/auth/utils/hasUserSignedIn'; +import { NextServer } from '../../../src/types'; +import { createMockNextApiResponse } from '../testUtils'; + +jest.mock('aws-amplify/auth/server'); + +const mockRunWithAmplifyServerContext = + jest.fn() as jest.MockedFunction; +const mockGetCurrentUser = jest.mocked(getCurrentUser); + +describe('hasUserSignedIn', () => { + const mockContextSpec = { token: { value: Symbol('mock') } }; + const mockCurrentUserResult: AuthUser = { + userId: 'mockUserId', + username: 'mockUsername', + }; + + beforeAll(() => { + mockRunWithAmplifyServerContext.mockImplementation( + async ({ nextServerContext: _, operation }) => { + return operation(mockContextSpec); + }, + ); + mockGetCurrentUser.mockResolvedValue(mockCurrentUserResult); + }); + + afterEach(() => { + mockRunWithAmplifyServerContext.mockClear(); + mockGetCurrentUser.mockClear(); + }); + + describe('hasUserSignedInWithAppRouter', () => { + const mockRequest = new NextRequest('https://example.com/api/auth/sign-in'); + + it('invokes server getCurrentUser() with expected parameter within the injected runWithAmplifyServerContext function', async () => { + await hasUserSignedInWithAppRouter({ + request: mockRequest, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(mockRunWithAmplifyServerContext).toHaveBeenCalledWith({ + nextServerContext: { + request: mockRequest, + response: expect.any(Response), + }, + operation: expect.any(Function), + }); + expect(mockGetCurrentUser).toHaveBeenCalledWith(mockContextSpec); + }); + + it('returns true when getCurrentUser() resolves (returned auth tokens)', async () => { + const result = await hasUserSignedInWithAppRouter({ + request: mockRequest, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(result).toBe(true); + }); + + it('returns false when getCurrentUser() rejects (no auth tokens)', async () => { + mockGetCurrentUser.mockRejectedValueOnce(new Error('No current user')); + + const result = await hasUserSignedInWithAppRouter({ + request: mockRequest, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(result).toBe(false); + }); + }); + + describe('hasUserSignedInWithPagesRouter', () => { + const mockRequest = { + url: 'https://example.com/api/auth/sign-in', + } as unknown as NextApiRequest; + const { mockResponse } = createMockNextApiResponse(); + + it('invokes server getCurrentUser() with expected parameter within the injected runWithAmplifyServerContext function', async () => { + await hasUserSignedInWithPagesRouter({ + request: mockRequest, + response: mockResponse, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(mockRunWithAmplifyServerContext).toHaveBeenCalledWith({ + nextServerContext: { + request: mockRequest, + response: mockResponse, + }, + operation: expect.any(Function), + }); + expect(mockGetCurrentUser).toHaveBeenCalledWith(mockContextSpec); + }); + + it('returns true when getCurrentUser() resolves (returned auth tokens)', async () => { + const result = await hasUserSignedInWithPagesRouter({ + request: mockRequest, + response: mockResponse, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(result).toBe(true); + }); + + it('returns false when getCurrentUser() rejects (no auth tokens)', async () => { + mockGetCurrentUser.mockRejectedValueOnce(new Error('No current user')); + + const result = await hasUserSignedInWithPagesRouter({ + request: mockRequest, + response: mockResponse, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(result).toBe(false); + }); + }); +}); diff --git a/packages/adapter-nextjs/__tests__/createServerRunner.test.ts b/packages/adapter-nextjs/__tests__/createServerRunner.test.ts index de2993188b9..cffa7ed0ccd 100644 --- a/packages/adapter-nextjs/__tests__/createServerRunner.test.ts +++ b/packages/adapter-nextjs/__tests__/createServerRunner.test.ts @@ -98,6 +98,7 @@ describe('createServerRunner', () => { expect(mockCreateAuthRouteHandlersFactory).toHaveBeenCalledWith({ config: mockAmplifyConfig, runtimeOptions: undefined, + runWithAmplifyServerContext: expect.any(Function), }); expect(result).toMatchObject({ createAuthRouteHandlers: expect.any(Function), diff --git a/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts b/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts index 9d1aac2668c..6e3b4389d7b 100644 --- a/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts +++ b/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts @@ -27,6 +27,7 @@ import { handleAuthApiRouteRequestForPagesRouter } from './handleAuthApiRouteReq export const createAuthRouteHandlersFactory = ({ config: resourcesConfig, runtimeOptions = {}, + runWithAmplifyServerContext, }: CreateAuthRouteHandlersFactoryInput): CreateAuthRouteHandlers => { const origin = process.env.AMPLIFY_APP_ORIGIN; if (!origin) @@ -65,6 +66,7 @@ export const createAuthRouteHandlersFactory = ({ oAuthConfig, setCookieOptions, origin, + runWithAmplifyServerContext, }); // In the Pages Router, the final response is handled by contextOrResponse @@ -84,6 +86,7 @@ export const createAuthRouteHandlersFactory = ({ oAuthConfig, setCookieOptions, origin, + runWithAmplifyServerContext, }); } diff --git a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts index af24955b71d..a37371a020a 100644 --- a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts +++ b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { HandleAuthApiRouteRequestForAppRouter } from './types'; -import { isSupportedAuthApiRoutePath } from './utils'; +import { + hasUserSignedInWithAppRouter, + isSupportedAuthApiRoutePath, +} from './utils'; import { handleSignInCallbackRequest, handleSignInSignUpRequest, @@ -19,6 +22,7 @@ export const handleAuthApiRouteRequestForAppRouter: HandleAuthApiRouteRequestFor oAuthConfig, origin, setCookieOptions, + runWithAmplifyServerContext, }) => { if (request.method !== 'GET') { return new Response(null, { status: 405 }); @@ -35,7 +39,21 @@ export const handleAuthApiRouteRequestForAppRouter: HandleAuthApiRouteRequestFor } switch (slug) { - case 'sign-up': + case 'sign-up': { + const hasUserSignedIn = await hasUserSignedInWithAppRouter({ + request, + runWithAmplifyServerContext, + }); + + if (hasUserSignedIn) { + return new Response(null, { + status: 302, + headers: new Headers({ + Location: handlerInput.redirectOnSignInComplete ?? '/', + }), + }); + } + return handleSignInSignUpRequest({ request, userPoolClientId, @@ -45,7 +63,22 @@ export const handleAuthApiRouteRequestForAppRouter: HandleAuthApiRouteRequestFor setCookieOptions, type: 'signUp', }); - case 'sign-in': + } + case 'sign-in': { + const hasUserSignedIn = await hasUserSignedInWithAppRouter({ + request, + runWithAmplifyServerContext, + }); + + if (hasUserSignedIn) { + return new Response(null, { + status: 302, + headers: new Headers({ + Location: handlerInput.redirectOnSignInComplete ?? '/', + }), + }); + } + return handleSignInSignUpRequest({ request, userPoolClientId, @@ -55,6 +88,7 @@ export const handleAuthApiRouteRequestForAppRouter: HandleAuthApiRouteRequestFor setCookieOptions, type: 'signIn', }); + } case 'sign-out': return handleSignOutRequest({ userPoolClientId, diff --git a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts index 6fe7db9c9c9..4bb3a3db7b2 100644 --- a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts +++ b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { HandleAuthApiRouteRequestForPagesRouter } from './types'; -import { isSupportedAuthApiRoutePath } from './utils'; +import { + hasUserSignedInWithPagesRouter, + isSupportedAuthApiRoutePath, +} from './utils'; import { handleSignInCallbackRequestForPagesRouter, handleSignInSignUpRequestForPagesRouter, @@ -19,6 +22,7 @@ export const handleAuthApiRouteRequestForPagesRouter: HandleAuthApiRouteRequestF handlerInput, origin, setCookieOptions, + runWithAmplifyServerContext, }) => { if (request.method !== 'GET') { response.status(405).end(); @@ -41,7 +45,19 @@ export const handleAuthApiRouteRequestForPagesRouter: HandleAuthApiRouteRequestF } switch (slug) { - case 'sign-up': + case 'sign-up': { + const hasUserSignedIn = await hasUserSignedInWithPagesRouter({ + request, + response, + runWithAmplifyServerContext, + }); + + if (hasUserSignedIn) { + response.redirect(302, handlerInput.redirectOnSignInComplete ?? '/'); + + return; + } + handleSignInSignUpRequestForPagesRouter({ request, response, @@ -53,7 +69,20 @@ export const handleAuthApiRouteRequestForPagesRouter: HandleAuthApiRouteRequestF type: 'signUp', }); break; + } case 'sign-in': { + const hasUserSignedIn = await hasUserSignedInWithPagesRouter({ + request, + response, + runWithAmplifyServerContext, + }); + + if (hasUserSignedIn) { + response.redirect(302, handlerInput.redirectOnSignInComplete ?? '/'); + + return; + } + handleSignInSignUpRequestForPagesRouter({ request, response, diff --git a/packages/adapter-nextjs/src/auth/types.ts b/packages/adapter-nextjs/src/auth/types.ts index 3f7ce5ff559..199e4eeb8f7 100644 --- a/packages/adapter-nextjs/src/auth/types.ts +++ b/packages/adapter-nextjs/src/auth/types.ts @@ -73,6 +73,7 @@ export type CreateAuthRouteHandlers = ( export interface CreateAuthRouteHandlersFactoryInput { config: ResourcesConfig; runtimeOptions: NextServer.CreateServerRunnerRuntimeOptions | undefined; + runWithAmplifyServerContext: NextServer.RunOperationWithContext; } export type CreateOAuthRouteHandlersFactory = ( @@ -91,12 +92,14 @@ interface HandleAuthApiRouteRequestForAppRouterInput extends HandleAuthApiRouteRequestInputBase { request: NextRequest; handlerContext: AuthRoutesHandlerContext; + runWithAmplifyServerContext: NextServer.RunOperationWithContext; } interface HandleAuthApiRouteRequestForPagesRouterInput extends HandleAuthApiRouteRequestInputBase { request: NextApiRequest; response: NextApiResponse; + runWithAmplifyServerContext: NextServer.RunOperationWithContext; } export type HandleAuthApiRouteRequestForAppRouter = ( diff --git a/packages/adapter-nextjs/src/auth/utils/hasUserSignedIn.ts b/packages/adapter-nextjs/src/auth/utils/hasUserSignedIn.ts new file mode 100644 index 00000000000..f3f7b3fe377 --- /dev/null +++ b/packages/adapter-nextjs/src/auth/utils/hasUserSignedIn.ts @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { NextRequest } from 'next/server'; +import { getCurrentUser } from 'aws-amplify/auth/server'; +import { NextApiRequest, NextApiResponse } from 'next'; + +import { NextServer } from '../../types'; + +export const hasUserSignedInWithAppRouter = async ({ + request, + runWithAmplifyServerContext, +}: { + request: NextRequest; + runWithAmplifyServerContext: NextServer.RunOperationWithContext; +}): Promise => { + const dummyResponse = new Response(); + + try { + await runWithAmplifyServerContext({ + nextServerContext: { request, response: dummyResponse }, + operation(contextSpec) { + return getCurrentUser(contextSpec); + }, + }); + + return true; + } catch (_) { + // `getCurrentUser()` throws if there is no valid token + return false; + } +}; + +export const hasUserSignedInWithPagesRouter = async ({ + request, + response, + runWithAmplifyServerContext, +}: { + request: NextApiRequest; + response: NextApiResponse; + runWithAmplifyServerContext: NextServer.RunOperationWithContext; +}): Promise => { + try { + await runWithAmplifyServerContext({ + nextServerContext: { request, response }, + operation(contextSpec) { + return getCurrentUser(contextSpec); + }, + }); + + return true; + } catch (_) { + // `getCurrentUser()` throws if there is no valid token + return false; + } +}; diff --git a/packages/adapter-nextjs/src/auth/utils/index.ts b/packages/adapter-nextjs/src/auth/utils/index.ts index 86d3da0b1a1..0cfce5f2ce6 100644 --- a/packages/adapter-nextjs/src/auth/utils/index.ts +++ b/packages/adapter-nextjs/src/auth/utils/index.ts @@ -29,6 +29,10 @@ export { isNextApiResponse, isNextRequest, } from './handlerParametersTypeAssertions'; +export { + hasUserSignedInWithAppRouter, + hasUserSignedInWithPagesRouter, +} from './hasUserSignedIn'; export { isSupportedAuthApiRoutePath } from './isSupportedAuthApiRoutePath'; export { resolveCodeAndStateFromUrl } from './resolveCodeAndStateFromUrl'; export { resolveIdentityProviderFromUrl } from './resolveIdentityProviderFromUrl'; diff --git a/packages/adapter-nextjs/src/createServerRunner.ts b/packages/adapter-nextjs/src/createServerRunner.ts index 4adc2a6bb2f..53364edfc7c 100644 --- a/packages/adapter-nextjs/src/createServerRunner.ts +++ b/packages/adapter-nextjs/src/createServerRunner.ts @@ -32,14 +32,17 @@ export const createServerRunner: NextServer.CreateServerRunner = ({ }) => { const amplifyConfig = parseAmplifyConfig(config); + const runWithAmplifyServerContext = createRunWithAmplifyServerContext({ + config: amplifyConfig, + runtimeOptions, + }); + return { - runWithAmplifyServerContext: createRunWithAmplifyServerContext({ - config: amplifyConfig, - runtimeOptions, - }), + runWithAmplifyServerContext, createAuthRouteHandlers: createAuthRouteHandlersFactory({ config: amplifyConfig, runtimeOptions, + runWithAmplifyServerContext, }), }; };