From bfcb7993d156d548f35ee7274e7e023c866c01af Mon Sep 17 00:00:00 2001 From: Giannis Katsanos Date: Fri, 28 Apr 2023 10:08:32 +0300 Subject: [PATCH 01/25] fix(clerk-js): Password settings maximum allowed length The setting for password maximum allowed length is 72. --- .../src/core/resources/UserSettings.test.ts | 22 +++++++++++++++++-- .../src/core/resources/UserSettings.ts | 10 +++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/core/resources/UserSettings.test.ts b/packages/clerk-js/src/core/resources/UserSettings.test.ts index dde1200ccbe..1cf7e8d0a84 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.test.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.test.ts @@ -56,7 +56,7 @@ describe('UserSettings', () => { expect(sut.instanceIsPasswordBased).toEqual(false); }); - it('respects default values for min and max length', function () { + it('respects default values for min and max password length', function () { let sut = new UserSettings({ attributes: { password: { @@ -72,7 +72,7 @@ describe('UserSettings', () => { expect(sut.passwordSettings).toMatchObject({ min_length: 8, - max_length: 100, + max_length: 72, }); sut = new UserSettings({ @@ -92,6 +92,24 @@ describe('UserSettings', () => { min_length: 10, max_length: 50, }); + + sut = new UserSettings({ + attributes: { + password: { + enabled: true, + required: false, + }, + }, + password_settings: { + min_length: 10, + max_length: 100, + }, + } as any as UserSettingsJSON); + + expect(sut.passwordSettings).toMatchObject({ + min_length: 10, + max_length: 72, + }); }); it('returns true if the instance has a valid auth factor', function () { diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts index 6ef8c12486f..334ad64bcb0 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.ts @@ -13,6 +13,9 @@ import type { import { BaseResource } from './internal'; +const defaultMaxPasswordLength = 72; +const defaultMinPasswordLength = 8; + /** * @internal */ @@ -66,8 +69,11 @@ export class UserSettings extends BaseResource implements UserSettingsResource { this.signUp = data.sign_up; this.passwordSettings = { ...data.password_settings, - min_length: Math.max(data?.password_settings?.min_length, 8), - max_length: data?.password_settings?.max_length === 0 ? 100 : data?.password_settings?.max_length, + min_length: Math.max(data?.password_settings?.min_length, defaultMinPasswordLength), + max_length: + data?.password_settings?.max_length === 0 + ? defaultMaxPasswordLength + : Math.min(data?.password_settings?.max_length, defaultMaxPasswordLength), }; this.socialProviderStrategies = this.getSocialProviderStrategies(data.social); this.authenticatableSocialStrategies = this.getAuthenticatableSocialStrategies(data.social); From 8d527bd624511d3b2c8a55605d4848c19429ae52 Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Fri, 28 Apr 2023 13:13:58 +0300 Subject: [PATCH 02/25] chore(nextjs): Use redirectToSignIn in app-router auth middleware --- packages/nextjs/src/server/authMiddleware.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index 7a17d4812d1..8a3fd89705d 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -7,6 +7,7 @@ import { NextResponse } from 'next/server'; import { isRedirect, mergeResponses, paths, setHeader } from '../utils'; import { authenticateRequest, handleInterstitialState, handleUnknownState } from './authenticateRequest'; import { receivedRequestForIgnoredRoute } from './errors'; +import { redirectToSignIn } from './redirect'; import type { NextMiddlewareResult, WithAuthOptions } from './types'; import { decorateRequest } from './utils'; @@ -161,10 +162,7 @@ export const createRouteMatcher = (routes: RouteMatcherParam) => { const createDefaultAfterAuth = (isPublicRoute: ReturnType) => { return (auth: AuthObject, req: NextRequest) => { if (!auth.userId && !isPublicRoute(req)) { - // TODO: replace with redirectToSignIn - const url = new URL(TMP_SIGN_IN_URL, req.nextUrl.origin); - url.searchParams.set('redirect_url', req.url); - return NextResponse.redirect(url.toString()); + return redirectToSignIn({ returnBackUrl: req.url }); } return NextResponse.next(); }; From 0e84519316c43770b2174c17a412854c335a3643 Mon Sep 17 00:00:00 2001 From: Matthias Posch Date: Fri, 28 Apr 2023 13:12:45 +0200 Subject: [PATCH 03/25] fix(localizations): Make emailAddresses GE translation consistent (#1117) * fix(localizations): Make emailAddresses GE translation consistent --------- Co-authored-by: Dimitris Klouvas --- packages/localizations/src/de-DE.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index 9913739c9f8..d22c0e26b88 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -4,7 +4,7 @@ export const deDE: LocalizationResource = { socialButtonsBlockButton: 'Weiter mit {{provider|titleize}}', dividerText: 'oder', formFieldLabel__emailAddress: 'E-Mail-Adresse', - formFieldLabel__emailAddresses: 'E-mailadressen', + formFieldLabel__emailAddresses: 'E-Mail-Adressen', formFieldLabel__phoneNumber: 'Telefonnummer', formFieldLabel__username: 'Nutzername', formFieldLabel__emailAddress_phoneNumber: 'E-Mail-Adresse oder Telefonnummer', From f09aeef57c9b3a8a790e60e9bd7dd47dc0be3517 Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Fri, 28 Apr 2023 14:27:48 +0300 Subject: [PATCH 04/25] chore(repo): Bump GH actions dependencies due to deprecation warning --- .github/workflows/ci.js.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.js.yml b/.github/workflows/ci.js.yml index 259e975a322..6672b486104 100644 --- a/.github/workflows/ci.js.yml +++ b/.github/workflows/ci.js.yml @@ -19,9 +19,9 @@ jobs: node-version: [16.19, 18.15, 19.8] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm i -g npm@8.19.0 From 4ba6c90d047a90e36cf21c42d255457d50373b32 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 28 Apr 2023 15:16:31 +0200 Subject: [PATCH 05/25] chore(clerk-js): Update english label for incorrect password --- packages/localizations/src/en-US.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 7cdad49c595..cc99977df0b 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -553,7 +553,7 @@ export const enUS: LocalizationResource = { form_password_incorrect: '', not_allowed_access: '', form_identifier_exists: '', - form_password_validation_failed: 'Invalid password supplied', + form_password_validation_failed: 'Incorrect Password', form_password_not_strong_enough: 'Your password is not strong enough.', passwordComplexity: { sentencePrefix: 'Your password must contain', From 61216228bde0113adf7aa66a091b13c4e2f1971b Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Fri, 28 Apr 2023 23:09:47 +0300 Subject: [PATCH 06/25] chore(nextjs): Add test coverage for app-router authMiddleware (#1124) --- .../nextjs/src/server/authMiddleware.test.ts | 257 +++++++++++++++++- packages/nextjs/src/server/authMiddleware.ts | 3 +- 2 files changed, 256 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/src/server/authMiddleware.test.ts b/packages/nextjs/src/server/authMiddleware.test.ts index 553255803d5..a2ed745d276 100644 --- a/packages/nextjs/src/server/authMiddleware.test.ts +++ b/packages/nextjs/src/server/authMiddleware.test.ts @@ -1,11 +1,45 @@ -import type { NextRequest } from 'next/server'; +// There is no need to execute the complete authenticateRequest to test authMiddleware +// This mock SHOULD exist before the import of authenticateRequest +jest.mock('./authenticateRequest', () => { + const { handleInterstitialState, handleUnknownState } = jest.requireActual('./authenticateRequest'); + return { + authenticateRequest: jest.fn().mockResolvedValue({ + toAuth: () => ({}), + }), + handleInterstitialState, + handleUnknownState, + }; +}); + +// Removing this mock will cause the authMiddleware tests to fail due to missing publishable key +// This mock SHOULD exist before the imports +jest.mock('./clerkClient', () => { + const { debugRequestState } = jest.requireActual('./clerkClient'); + return { + PUBLISHABLE_KEY: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', + clerkClient: { + localInterstitial: jest.fn().mockResolvedValue('interstitial'), + }, + debugRequestState, + }; +}); + +import type { NextFetchEvent, NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; import { paths } from '../utils'; -import { createRouteMatcher, DEFAULT_CONFIG_MATCHER, DEFAULT_IGNORED_ROUTES } from './authMiddleware'; +// used to assert the mock +import { authenticateRequest } from './authenticateRequest'; +import { authMiddleware, createRouteMatcher, DEFAULT_CONFIG_MATCHER, DEFAULT_IGNORED_ROUTES } from './authMiddleware'; +// used to assert the mock +import { clerkClient } from './clerkClient'; const mockRequest = (url: string) => { return { + url: new URL(url, 'https://www.clerk.com').toString(), nextUrl: new URL(url, 'https://www.clerk.com'), + cookies: {}, + headers: new Headers(), } as NextRequest; }; @@ -76,3 +110,222 @@ describe('default matcher', () => { expect(matcher(mockRequest('/_next/test.json'))).toBe(true); }); }); + +describe('authMiddleware(params)', () => { + beforeEach(() => { + // @ts-ignore + authenticateRequest.mockClear(); + // @ts-ignore + clerkClient.localInterstitial.mockClear(); + }); + + describe('without params', function () { + it('redirects to sign-in for protected route', async () => { + const resp = await authMiddleware()(mockRequest('/protected'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(307); + expect(resp?.headers.get('location')).toEqual( + 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', + ); + }); + + it('renders public route', async () => { + const signInResp = await authMiddleware()(mockRequest('/sign-in'), {} as NextFetchEvent); + expect(signInResp?.status).toEqual(200); + expect(signInResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/sign-in'); + + const signUpResp = await authMiddleware()(mockRequest('/sign-up'), {} as NextFetchEvent); + expect(signUpResp?.status).toEqual(200); + expect(signUpResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/sign-up'); + }); + }); + + describe('with ignoredRoutes', function () { + it('skips auth middleware execution', async () => { + const beforeAuthSpy = jest.fn(); + const afterAuthSpy = jest.fn(); + const resp = await authMiddleware({ + ignoredRoutes: '/ignored', + beforeAuth: beforeAuthSpy, + afterAuth: afterAuthSpy, + })(mockRequest('/ignored'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(200); + expect(authenticateRequest).not.toBeCalled(); + expect(beforeAuthSpy).not.toBeCalled(); + expect(afterAuthSpy).not.toBeCalled(); + }); + + it('executes auth middleware execution when is not matched', async () => { + const beforeAuthSpy = jest.fn(); + const afterAuthSpy = jest.fn(); + const resp = await authMiddleware({ + ignoredRoutes: '/ignored', + beforeAuth: beforeAuthSpy, + afterAuth: afterAuthSpy, + })(mockRequest('/protected'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(200); + expect(authenticateRequest).toBeCalled(); + expect(beforeAuthSpy).toBeCalled(); + expect(afterAuthSpy).toBeCalled(); + }); + }); + + describe('with publicRoutes', function () { + it('renders public route', async () => { + const resp = await authMiddleware({ + publicRoutes: '/public', + })(mockRequest('/public'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(200); + expect(resp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/public'); + }); + + it('renders sign-in/sing-up routes', async () => { + const signInResp = await authMiddleware({ + publicRoutes: '/public', + })(mockRequest('/sign-in'), {} as NextFetchEvent); + expect(signInResp?.status).toEqual(200); + expect(signInResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/sign-in'); + + const signUpResp = await authMiddleware({ + publicRoutes: '/public', + })(mockRequest('/sign-up'), {} as NextFetchEvent); + expect(signUpResp?.status).toEqual(200); + expect(signUpResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/sign-up'); + }); + + it('redirects to sign-in for protected route', async () => { + const resp = await authMiddleware({ + publicRoutes: '/public', + })(mockRequest('/protected'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(307); + expect(resp?.headers.get('location')).toEqual( + 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', + ); + }); + }); + + describe('with beforeAuth', function () { + it('skips auth middleware execution when beforeAuth returns false', async () => { + const afterAuthSpy = jest.fn(); + const resp = await authMiddleware({ + beforeAuth: () => false, + afterAuth: afterAuthSpy, + })(mockRequest('/protected'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(200); + expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('skip'); + expect(authenticateRequest).not.toBeCalled(); + expect(afterAuthSpy).not.toBeCalled(); + }); + + it('executes auth middleware execution when beforeAuth returns undefined', async () => { + const afterAuthSpy = jest.fn(); + const resp = await authMiddleware({ + beforeAuth: () => undefined, + afterAuth: afterAuthSpy, + })(mockRequest('/protected'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(200); + expect(authenticateRequest).toBeCalled(); + expect(afterAuthSpy).toBeCalled(); + }); + + it('skips auth middleware execution when beforeAuth returns NextResponse.redirect', async () => { + const afterAuthSpy = jest.fn(); + const resp = await authMiddleware({ + beforeAuth: () => NextResponse.redirect('https://www.clerk.com/custom-redirect'), + afterAuth: afterAuthSpy, + })(mockRequest('/protected'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(307); + expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/custom-redirect'); + expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('redirect'); + expect(authenticateRequest).not.toBeCalled(); + expect(afterAuthSpy).not.toBeCalled(); + }); + + it('executes auth middleware when beforeAuth returns NextResponse', async () => { + const resp = await authMiddleware({ + beforeAuth: () => + NextResponse.next({ + headers: { + 'x-before-auth-header': 'before', + }, + }), + afterAuth: () => + NextResponse.next({ + headers: { + 'x-after-auth-header': 'after', + }, + }), + })(mockRequest('/protected'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(200); + expect(resp?.headers.get('x-before-auth-header')).toEqual('before'); + expect(resp?.headers.get('x-after-auth-header')).toEqual('after'); + expect(authenticateRequest).toBeCalled(); + }); + }); + + describe('with afterAuth', function () { + it('redirects to sign-in for protected route and sets redirect as auth reason header', async () => { + const resp = await authMiddleware({ + beforeAuth: () => NextResponse.next(), + })(mockRequest('/protected'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(307); + expect(resp?.headers.get('location')).toEqual( + 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', + ); + expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('redirect'); + expect(authenticateRequest).toBeCalled(); + }); + + it('uses authenticateRequest result as auth', async () => { + const req = mockRequest('/protected'); + const event = {} as NextFetchEvent; + // @ts-ignore + authenticateRequest.mockResolvedValueOnce({ toAuth: () => ({ userId: null }) }); + const afterAuthSpy = jest.fn(); + + await authMiddleware({ afterAuth: afterAuthSpy })(req, event); + + expect(authenticateRequest).toBeCalled(); + expect(afterAuthSpy).toBeCalledWith( + { + userId: null, + isPublicRoute: false, + }, + req, + event, + ); + }); + }); + + describe('authenticateRequest', function () { + it('returns 401 with local interstitial for interstitial requestState', async () => { + // @ts-ignore + authenticateRequest.mockResolvedValueOnce({ isInterstitial: true }); + const resp = await authMiddleware()(mockRequest('/protected'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(401); + expect(resp?.headers.get('content-type')).toEqual('text/html'); + expect(clerkClient.localInterstitial).toBeCalled(); + }); + + it('returns 401 for unknown requestState', async () => { + // @ts-ignore + authenticateRequest.mockResolvedValueOnce({ isUnknown: true }); + const resp = await authMiddleware()(mockRequest('/protected'), {} as NextFetchEvent); + + expect(resp?.status).toEqual(401); + expect(resp?.body).toBeNull(); + expect(resp?.headers.get('content-type')).toEqual('text/html'); + expect(clerkClient.localInterstitial).not.toBeCalled(); + }); + }); +}); diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index 8a3fd89705d..ce4873a0966 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -93,8 +93,7 @@ type AuthMiddlewareParams = WithAuthOptions & { }; export interface AuthMiddleware { - (): NextMiddleware; - (params: AuthMiddlewareParams): NextMiddleware; + (params?: AuthMiddlewareParams): NextMiddleware; } const authMiddleware: AuthMiddleware = (...args: unknown[]) => { From d87493d13e2c7a3ffbf37ba728e6cde7f6f14682 Mon Sep 17 00:00:00 2001 From: George Desipris <73396808+desiprisg@users.noreply.github.com> Date: Fri, 28 Apr 2023 23:22:48 +0300 Subject: [PATCH 07/25] fix(clerk-js,types): Remove after_sign_out_url as it not returned by FAPI (#1121) --- packages/clerk-js/src/core/resources/DisplayConfig.ts | 1 - packages/types/src/displayConfig.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/clerk-js/src/core/resources/DisplayConfig.ts b/packages/clerk-js/src/core/resources/DisplayConfig.ts index df7de46ce05..3c5a3178496 100644 --- a/packages/clerk-js/src/core/resources/DisplayConfig.ts +++ b/packages/clerk-js/src/core/resources/DisplayConfig.ts @@ -53,7 +53,6 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource this.userProfileUrl = data.user_profile_url; this.afterSignInUrl = data.after_sign_in_url; this.afterSignUpUrl = data.after_sign_up_url; - this.afterSignOutUrl = data.after_sign_out_url; this.afterSignOutOneUrl = data.after_sign_out_one_url; this.afterSignOutAllUrl = data.after_sign_out_all_url; this.afterSwitchSessionUrl = data.after_switch_session_url; diff --git a/packages/types/src/displayConfig.ts b/packages/types/src/displayConfig.ts index f2abd92b722..01e25539fa4 100644 --- a/packages/types/src/displayConfig.ts +++ b/packages/types/src/displayConfig.ts @@ -9,7 +9,6 @@ export interface DisplayConfigJSON { after_sign_in_url: string; after_sign_out_all_url: string; after_sign_out_one_url: string; - after_sign_out_url: string; after_sign_up_url: string; after_switch_session_url: string; application_name: string; From dd4a68d59f7d2d4821630a5d229519c2b58cb66b Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Tue, 2 May 2023 13:24:35 +0300 Subject: [PATCH 08/25] chore(nextjs): Reuse props, useInvalidateCacheOnAuthChange and useInvokeMiddlewareOnAuthChange (#1125) --- .../src/app-router/client/ClerkProvider.tsx | 61 +++----- .../app-router/client/useAwaitableNavigate.ts | 35 +++++ .../src/app-router/server/ClerkProvider.tsx | 13 +- .../client-boundary/NextOptionsContext.tsx | 20 +++ .../src/client-boundary/uiComponents.ts | 13 -- .../src/client-boundary/uiComponents.tsx | 45 ++++++ .../useInvalidateCacheOnAuthChange.tsx | 8 ++ .../useInvokeMiddlewareOnAuthChange.tsx | 12 ++ .../client-boundary/useSafeLayoutEffect.tsx | 4 + packages/nextjs/src/pages/ClerkProvider.tsx | 134 ++++-------------- .../nextjs/src/pages/NextProviderContext.tsx | 21 --- .../__snapshots__/exports.test.ts.snap | 2 - packages/nextjs/src/server/authMiddleware.ts | 2 +- packages/nextjs/src/types.ts | 19 +++ .../invalidateNextRouterCache.ts | 0 .../src/utils/mergeNextClerkPropsWithEnv.ts | 17 +++ 16 files changed, 217 insertions(+), 189 deletions(-) create mode 100644 packages/nextjs/src/app-router/client/useAwaitableNavigate.ts create mode 100644 packages/nextjs/src/client-boundary/NextOptionsContext.tsx delete mode 100644 packages/nextjs/src/client-boundary/uiComponents.ts create mode 100644 packages/nextjs/src/client-boundary/uiComponents.tsx create mode 100644 packages/nextjs/src/client-boundary/useInvalidateCacheOnAuthChange.tsx create mode 100644 packages/nextjs/src/client-boundary/useInvokeMiddlewareOnAuthChange.tsx create mode 100644 packages/nextjs/src/client-boundary/useSafeLayoutEffect.tsx delete mode 100644 packages/nextjs/src/pages/NextProviderContext.tsx create mode 100644 packages/nextjs/src/types.ts rename packages/nextjs/src/{pages => utils}/invalidateNextRouterCache.ts (100%) create mode 100644 packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index f122a0ff00f..abc391634d9 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -1,9 +1,12 @@ 'use client'; -// !!! Note the import from react -import type { ClerkProviderProps } from '@clerk/clerk-react'; import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react'; -import { usePathname, useRouter } from 'next/navigation'; -import React, { useCallback, useEffect } from 'react'; +import React from 'react'; + +import { ClerkNextOptionsProvider } from '../../client-boundary/NextOptionsContext'; +import { useInvalidateCacheOnAuthChange } from '../../client-boundary/useInvalidateCacheOnAuthChange'; +import { useInvokeMiddlewareOnAuthChange } from '../../client-boundary/useInvokeMiddlewareOnAuthChange'; +import type { NextClerkProviderProps } from '../../types'; +import { useAwaitableNavigate } from './useAwaitableNavigate'; declare global { export interface Window { @@ -12,43 +15,23 @@ declare global { } } -const useAwaitableNavigate = () => { - // eslint-disable-next-line @typescript-eslint/unbound-method - const { push, refresh } = useRouter(); - const pathname = usePathname(); - - useEffect(() => { - window.__clerk_nav = (to: string) => { - return new Promise(res => { - window.__clerk_nav_await.push(res); - if (to === pathname) { - refresh(); - } else { - push(to); - } - }); - }; - }, [pathname]); - - useEffect(() => { - if (typeof window.__clerk_nav_await === 'undefined') { - window.__clerk_nav_await = []; - } - window.__clerk_nav_await.forEach(resolve => resolve()); - window.__clerk_nav_await = []; +export const ClientClerkProvider = (props: NextClerkProviderProps) => { + const { __unstable_invokeMiddlewareOnAuthStateChange = true } = props; + const navigate = useAwaitableNavigate(); + useInvalidateCacheOnAuthChange(); + useInvokeMiddlewareOnAuthChange({ + invoke: () => { + if (__unstable_invokeMiddlewareOnAuthStateChange) { + void navigate(window.location.href); + } + }, }); - return useCallback((to: string) => { - return window.__clerk_nav(to); - }, []); -}; - -export const ClientClerkProvider = (props: ClerkProviderProps) => { - const navigate = useAwaitableNavigate(); + const mergedProps = { ...props, navigate }; return ( - + + {/*// @ts-ignore*/} + + ); }; diff --git a/packages/nextjs/src/app-router/client/useAwaitableNavigate.ts b/packages/nextjs/src/app-router/client/useAwaitableNavigate.ts new file mode 100644 index 00000000000..7cee5303517 --- /dev/null +++ b/packages/nextjs/src/app-router/client/useAwaitableNavigate.ts @@ -0,0 +1,35 @@ +'use client'; + +import { usePathname, useRouter } from 'next/navigation'; +import { useCallback, useEffect } from 'react'; + +export const useAwaitableNavigate = () => { + // eslint-disable-next-line @typescript-eslint/unbound-method + const { push, refresh } = useRouter(); + const pathname = usePathname(); + + useEffect(() => { + window.__clerk_nav = (to: string) => { + return new Promise(res => { + window.__clerk_nav_await.push(res); + if (to === pathname) { + refresh(); + } else { + push(to); + } + }); + }; + }, [pathname]); + + useEffect(() => { + if (typeof window.__clerk_nav_await === 'undefined') { + window.__clerk_nav_await = []; + } + window.__clerk_nav_await.forEach(resolve => resolve()); + window.__clerk_nav_await = []; + }); + + return useCallback((to: string) => { + return window.__clerk_nav(to); + }, []); +}; diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 59af6e22490..71996bca197 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -2,6 +2,7 @@ import type { IsomorphicClerkOptions } from '@clerk/clerk-react/dist/types'; import type { InitialState, PublishableKeyOrFrontendApi } from '@clerk/types'; import React from 'react'; +import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; import { ClientClerkProvider } from '../client/ClerkProvider'; import { initialState } from './auth'; @@ -10,16 +11,16 @@ type NextAppClerkProviderProps = React.PropsWithChildren< >; export function ClerkProvider(props: NextAppClerkProviderProps) { + const { children, ...rest } = props; const state = initialState()?.__clerk_ssr_state as InitialState; return ( - // @ts-ignore + > + {children} + ); } diff --git a/packages/nextjs/src/client-boundary/NextOptionsContext.tsx b/packages/nextjs/src/client-boundary/NextOptionsContext.tsx new file mode 100644 index 00000000000..1de42eec164 --- /dev/null +++ b/packages/nextjs/src/client-boundary/NextOptionsContext.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import type { NextClerkProviderProps } from '../types'; + +type ClerkNextContextValue = Partial>; + +const ClerkNextOptionsCtx = React.createContext<{ value: ClerkNextContextValue } | undefined>(undefined); +ClerkNextOptionsCtx.displayName = 'ClerkNextOptionsCtx'; + +const useClerkNextOptions = () => { + const ctx = React.useContext(ClerkNextOptionsCtx)!; + return ctx.value as ClerkNextContextValue; +}; + +const ClerkNextOptionsProvider = (props: React.PropsWithChildren<{ options: NextClerkProviderProps }>) => { + const { children, options } = props; + return {children}; +}; + +export { ClerkNextOptionsProvider, useClerkNextOptions }; diff --git a/packages/nextjs/src/client-boundary/uiComponents.ts b/packages/nextjs/src/client-boundary/uiComponents.ts deleted file mode 100644 index 04a52604b8c..00000000000 --- a/packages/nextjs/src/client-boundary/uiComponents.ts +++ /dev/null @@ -1,13 +0,0 @@ -'use client'; - -export { - SignIn, - SignUp, - UserProfile, - UserButton, - OrganizationSwitcher, - OrganizationProfile, - CreateOrganization, - SignInButton, - SignUpButton, -} from '@clerk/clerk-react'; diff --git a/packages/nextjs/src/client-boundary/uiComponents.tsx b/packages/nextjs/src/client-boundary/uiComponents.tsx new file mode 100644 index 00000000000..74309c247ac --- /dev/null +++ b/packages/nextjs/src/client-boundary/uiComponents.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { SignIn as BaseSignIn, SignUp as BaseSignUp } from '@clerk/clerk-react'; +import type { SignInProps, SignUpProps } from '@clerk/types'; +import React from 'react'; + +import { useClerkNextOptions } from './NextOptionsContext'; + +export { + UserProfile, + UserButton, + OrganizationSwitcher, + OrganizationProfile, + CreateOrganization, + SignInButton, + SignUpButton, +} from '@clerk/clerk-react'; + +export const SignIn = (props: SignInProps) => { + const { signInUrl: repoLevelSignInUrl } = useClerkNextOptions(); + if (repoLevelSignInUrl) { + return ( + + ); + } + return ; +}; + +export const SignUp = (props: SignUpProps) => { + const { signUpUrl: repoLevelSignUpUrl } = useClerkNextOptions(); + if (repoLevelSignUpUrl) { + return ( + + ); + } + return ; +}; diff --git a/packages/nextjs/src/client-boundary/useInvalidateCacheOnAuthChange.tsx b/packages/nextjs/src/client-boundary/useInvalidateCacheOnAuthChange.tsx new file mode 100644 index 00000000000..c6c85fe59ed --- /dev/null +++ b/packages/nextjs/src/client-boundary/useInvalidateCacheOnAuthChange.tsx @@ -0,0 +1,8 @@ +import { invalidateNextRouterCache } from '../utils/invalidateNextRouterCache'; +import { useSafeLayoutEffect } from './useSafeLayoutEffect'; + +export const useInvalidateCacheOnAuthChange = () => { + useSafeLayoutEffect(() => { + window.__unstable__onBeforeSetActive = invalidateNextRouterCache; + }, []); +}; diff --git a/packages/nextjs/src/client-boundary/useInvokeMiddlewareOnAuthChange.tsx b/packages/nextjs/src/client-boundary/useInvokeMiddlewareOnAuthChange.tsx new file mode 100644 index 00000000000..e2bfef93163 --- /dev/null +++ b/packages/nextjs/src/client-boundary/useInvokeMiddlewareOnAuthChange.tsx @@ -0,0 +1,12 @@ +import { useSafeLayoutEffect } from './useSafeLayoutEffect'; + +export const useInvokeMiddlewareOnAuthChange = (opts: { invoke: () => void }) => { + useSafeLayoutEffect(() => { + window.__unstable__onAfterSetActive = () => { + // Re-run the middleware every time there auth state changes. + // This enables complete control from a centralised place (NextJS middleware), + // as we will invoke it every time the client-side auth state changes, eg: signing-out, switching orgs, etc.\ + opts.invoke(); + }; + }, []); +}; diff --git a/packages/nextjs/src/client-boundary/useSafeLayoutEffect.tsx b/packages/nextjs/src/client-boundary/useSafeLayoutEffect.tsx new file mode 100644 index 00000000000..119fc46a6bb --- /dev/null +++ b/packages/nextjs/src/client-boundary/useSafeLayoutEffect.tsx @@ -0,0 +1,4 @@ +import React from 'react'; + +// TODO: Import from shared once [JS-118] is done +export const useSafeLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; diff --git a/packages/nextjs/src/pages/ClerkProvider.tsx b/packages/nextjs/src/pages/ClerkProvider.tsx index a1a7cd21fd6..51bf88a1c15 100644 --- a/packages/nextjs/src/pages/ClerkProvider.tsx +++ b/packages/nextjs/src/pages/ClerkProvider.tsx @@ -1,125 +1,45 @@ -import { - __internal__setErrorThrowerOptions, - ClerkProvider as ReactClerkProvider, - SignIn as BaseSignIn, - SignUp as BaseSignUp, -} from '@clerk/clerk-react'; -import type { IsomorphicClerkOptions } from '@clerk/clerk-react/dist/types'; -import type { MultiDomainAndOrProxy, PublishableKeyOrFrontendApi, SignInProps, SignUpProps } from '@clerk/types'; +import { __internal__setErrorThrowerOptions, ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react'; import { useRouter } from 'next/router'; import React from 'react'; -import { invalidateNextRouterCache } from './invalidateNextRouterCache'; -import { ClerkNextProvider, useClerkNextContext } from './NextProviderContext'; - -// TODO: Import from shared once [JS-118] is done -const useSafeLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; +import { ClerkNextOptionsProvider } from '../client-boundary/NextOptionsContext'; +import { useInvalidateCacheOnAuthChange } from '../client-boundary/useInvalidateCacheOnAuthChange'; +import { useInvokeMiddlewareOnAuthChange } from '../client-boundary/useInvokeMiddlewareOnAuthChange'; +import type { NextClerkProviderProps } from '../types'; +import { mergeNextClerkPropsWithEnv } from '../utils/mergeNextClerkPropsWithEnv'; __internal__setErrorThrowerOptions({ packageName: '@clerk/nextjs' }); -export const SignIn = (props: SignInProps) => { - const { signInUrl } = useClerkNextContext(); - if (signInUrl) { - return ( - - ); - } - return ; -}; - -export const SignUp = (props: SignUpProps) => { - const { signUpUrl } = useClerkNextContext(); - if (signUpUrl) { - return ( - - ); - } - return ; -}; - -type NextClerkProviderProps = { - children: React.ReactNode; - /** - * If set to true, the NextJS middleware will be invoked - * every time the client-side auth state changes (sign-out, sign-in, organization switch etc.). - * That way, any auth-dependent logic can be placed inside the middleware. - * Example: Configuring the middleware to force a redirect to `/sign-in` when the user signs out - * - * @default true - */ - __unstable_invokeMiddlewareOnAuthStateChange?: boolean; -} & Omit & - Partial & - Omit & - MultiDomainAndOrProxy; - -export function ClerkProvider({ children, ...rest }: NextClerkProviderProps): JSX.Element { - // Allow for overrides without making the type public - const { - // @ts-expect-error - authServerSideProps, - frontendApi, - publishableKey, - proxyUrl, - domain, - isSatellite, - signInUrl, - signUpUrl, - afterSignInUrl, - afterSignUpUrl, - // @ts-expect-error - __clerk_ssr_state, - clerkJSUrl, - __unstable_invokeMiddlewareOnAuthStateChange = true, - ...restProps - } = rest; +export function ClerkProvider({ children, ...props }: NextClerkProviderProps): JSX.Element { + const { __unstable_invokeMiddlewareOnAuthStateChange = true } = props; const { push } = useRouter(); - ReactClerkProvider.displayName = 'ReactClerkProvider'; - useSafeLayoutEffect(() => { - window.__unstable__onBeforeSetActive = invalidateNextRouterCache; - window.__unstable__onAfterSetActive = () => { - // Re-run the middleware every time there auth state changes. - // This enables complete control from a centralised place (NextJS middleware), - // as we will invoke it every time the client-side auth state changes, eg: signing-out, switching orgs, etc. + useInvalidateCacheOnAuthChange(); + useInvokeMiddlewareOnAuthChange({ + invoke: () => { if (__unstable_invokeMiddlewareOnAuthStateChange) { void push(window.location.href); } - }; - }, []); + }, + }); - const nextProps = { - frontendApi: frontendApi || process.env.NEXT_PUBLIC_CLERK_FRONTEND_API || '', - publishableKey: publishableKey || process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || '', - clerkJSUrl: clerkJSUrl || process.env.NEXT_PUBLIC_CLERK_JS, - proxyUrl: proxyUrl || process.env.NEXT_PUBLIC_CLERK_PROXY_URL || '', - domain: domain || process.env.NEXT_PUBLIC_CLERK_DOMAIN || '', - isSatellite: isSatellite || process.env.NEXT_PUBLIC_CLERK_IS_SATELLITE === 'true', - signInUrl: signInUrl || process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL || '', - signUpUrl: signUpUrl || process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL || '', - afterSignInUrl: afterSignInUrl || process.env.NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL || '', - afterSignUpUrl: afterSignUpUrl || process.env.NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL || '', - navigate: (to: string) => push(to), - // withServerSideAuth automatically injects __clerk_ssr_state - // getAuth returns a user-facing authServerSideProps that hides __clerk_ssr_state - initialState: authServerSideProps?.__clerk_ssr_state || __clerk_ssr_state, - ...restProps, - }; + const navigate = (to: string) => push(to); + const mergedProps = mergeNextClerkPropsWithEnv({ ...props, navigate }); + // withServerSideAuth automatically injects __clerk_ssr_state + // getAuth returns a user-facing authServerSideProps that hides __clerk_ssr_state + // @ts-expect-error initialState is hidden from the types as it's a private prop + const initialState = props.authServerSideProps?.__clerk_ssr_state || props.__clerk_ssr_state; return ( - // @ts-expect-error - + {/*@ts-expect-error*/} - {children} - + + {children} + + ); } diff --git a/packages/nextjs/src/pages/NextProviderContext.tsx b/packages/nextjs/src/pages/NextProviderContext.tsx deleted file mode 100644 index 30f2fff1b07..00000000000 --- a/packages/nextjs/src/pages/NextProviderContext.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { ClerkProviderProps } from '@clerk/clerk-react'; -import React from 'react'; - -type ClerkNextContextValue = Omit; - -const ClerkNextContext = React.createContext<{ value: ClerkNextContextValue } | undefined>(undefined); -ClerkNextContext.displayName = 'ClerkNextContext'; - -const useClerkNextContext = () => { - const ctx = React.useContext(ClerkNextContext); - return (ctx as any).value as ClerkNextContextValue; -}; - -const ClerkNextProvider = (props: ClerkProviderProps) => { - const { children, ...restProps } = props; - const ctxValue = { value: restProps }; - - return {children}; -}; - -export { ClerkNextProvider, useClerkNextContext }; diff --git a/packages/nextjs/src/pages/__tests__/__snapshots__/exports.test.ts.snap b/packages/nextjs/src/pages/__tests__/__snapshots__/exports.test.ts.snap index 880eee5005b..2c86f91cb5e 100644 --- a/packages/nextjs/src/pages/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/nextjs/src/pages/__tests__/__snapshots__/exports.test.ts.snap @@ -3,7 +3,5 @@ exports[`/client public exports should not include a breaking change 1`] = ` Object { "ClerkProvider": [Function], - "SignIn": [Function], - "SignUp": [Function], } `; diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index ce4873a0966..b8b3fe3372c 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -52,7 +52,7 @@ type IgnoredRoutesParam = Array | RegExp | string | ((req: Next type BeforeAuthHandler = ( req: NextRequest, evt: NextFetchEvent, -) => NextMiddlewareResult | Promise | false; +) => NextMiddlewareResult | Promise | false | Promise; type AfterAuthHandler = ( auth: AuthObject & { isPublicRoute: boolean }, diff --git a/packages/nextjs/src/types.ts b/packages/nextjs/src/types.ts new file mode 100644 index 00000000000..2818beff63f --- /dev/null +++ b/packages/nextjs/src/types.ts @@ -0,0 +1,19 @@ +import type { IsomorphicClerkOptions } from '@clerk/clerk-react/dist/types'; +import type { MultiDomainAndOrProxy, PublishableKeyOrFrontendApi } from '@clerk/types'; +import type React from 'react'; + +export type NextClerkProviderProps = { + children: React.ReactNode; + /** + * If set to true, the NextJS middleware will be invoked + * every time the client-side auth state changes (sign-out, sign-in, organization switch etc.). + * That way, any auth-dependent logic can be placed inside the middleware. + * Example: Configuring the middleware to force a redirect to `/sign-in` when the user signs out + * + * @default true + */ + __unstable_invokeMiddlewareOnAuthStateChange?: boolean; +} & Omit & + Partial & + Omit & + MultiDomainAndOrProxy; diff --git a/packages/nextjs/src/pages/invalidateNextRouterCache.ts b/packages/nextjs/src/utils/invalidateNextRouterCache.ts similarity index 100% rename from packages/nextjs/src/pages/invalidateNextRouterCache.ts rename to packages/nextjs/src/utils/invalidateNextRouterCache.ts diff --git a/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts b/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts new file mode 100644 index 00000000000..06aecd2d308 --- /dev/null +++ b/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts @@ -0,0 +1,17 @@ +import type { NextClerkProviderProps } from '../types'; + +export const mergeNextClerkPropsWithEnv = (props: Omit) => { + return { + ...props, + frontendApi: props.frontendApi || process.env.NEXT_PUBLIC_CLERK_FRONTEND_API || '', + publishableKey: props.publishableKey || process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || '', + clerkJSUrl: props.clerkJSUrl || process.env.NEXT_PUBLIC_CLERK_JS, + proxyUrl: props.proxyUrl || process.env.NEXT_PUBLIC_CLERK_PROXY_URL || '', + domain: props.domain || process.env.NEXT_PUBLIC_CLERK_DOMAIN || '', + isSatellite: props.isSatellite || process.env.NEXT_PUBLIC_CLERK_IS_SATELLITE === 'true', + signInUrl: props.signInUrl || process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL || '', + signUpUrl: props.signUpUrl || process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL || '', + afterSignInUrl: props.afterSignInUrl || process.env.NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL || '', + afterSignUpUrl: props.afterSignUpUrl || process.env.NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL || '', + } as any as NextClerkProviderProps; +}; From 823a0c0c2e83cff1e4c2793994c6a4069881b568 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 2 May 2023 13:19:50 +0200 Subject: [PATCH 09/25] fix(clerk-js): Remove forgotten console.log --- packages/clerk-js/src/ui/elements/Form.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/clerk-js/src/ui/elements/Form.tsx b/packages/clerk-js/src/ui/elements/Form.tsx index c7f1d3f9e05..657f1c2425c 100644 --- a/packages/clerk-js/src/ui/elements/Form.tsx +++ b/packages/clerk-js/src/ui/elements/Form.tsx @@ -60,7 +60,6 @@ const FormRoot = (props: FormProps): JSX.Element => { */}