-
Notifications
You must be signed in to change notification settings - Fork 408
chore(tanstack-react-start): Reuse existing auth object from server handler #6595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6f34fb7
552e365
bac150f
c715e08
a4ee4b1
580e2a5
4d62749
9af56de
c2faffe
6dd884f
5764683
1f5ea7e
77b6343
b4b54ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| --- | ||
| "@clerk/tanstack-react-start": minor | ||
| --- | ||
wobsoriano marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Reuses existing `Auth` object from the server handler when using `getAuth()` | ||
|
|
||
| The `createClerkHandler` helper now returns a Promise and requires awaiting during setup to ensure authentication context is available at the earliest possible point in the request lifecycle, before any router loaders or server functions execute | ||
|
|
||
| ```ts | ||
| // server.ts | ||
| import { createStartHandler, defineHandlerCallback, defaultStreamHandler } from '@tanstack/react-start/server'; | ||
| import { createRouter } from './router'; | ||
| import { createClerkHandler } from '@clerk/tanstack-react-start/server'; | ||
|
|
||
| const handlerFactory = createClerkHandler( | ||
| createStartHandler({ | ||
| createRouter, | ||
| }), | ||
| ); | ||
|
|
||
| export default defineHandlerCallback(async event => { | ||
| const startHandler = await handlerFactory(defaultStreamHandler); // awaited | ||
| return startHandler(event); | ||
| }); | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,14 @@ | ||
| import { createStartHandler, defaultStreamHandler } from '@tanstack/react-start/server'; | ||
| import { createStartHandler, defineHandlerCallback, defaultStreamHandler } from '@tanstack/react-start/server'; | ||
| import { createRouter } from './router'; | ||
| import { createClerkHandler } from '@clerk/tanstack-react-start/server'; | ||
|
|
||
| export default createClerkHandler( | ||
| const handlerFactory = createClerkHandler( | ||
| createStartHandler({ | ||
| createRouter, | ||
| }), | ||
| )(defaultStreamHandler); | ||
| ); | ||
|
|
||
| export default defineHandlerCallback(async event => { | ||
| const startHandler = await handlerFactory(defaultStreamHandler); | ||
| return startHandler(event); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,25 @@ | ||
| import type { AuthenticateRequestOptions, GetAuthFn } from '@clerk/backend/internal'; | ||
| import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal'; | ||
| import { getContext } from '@tanstack/react-start/server'; | ||
|
|
||
| import { errorThrower } from '../utils'; | ||
| import { noFetchFnCtxPassedInGetAuth } from '../utils/errors'; | ||
| import { authenticateRequest } from './authenticateRequest'; | ||
| import { loadOptions } from './loadOptions'; | ||
| import type { LoaderOptions } from './types'; | ||
| import { clerkHandlerNotConfigured, noFetchFnCtxPassedInGetAuth } from '../utils/errors'; | ||
|
|
||
| type GetAuthOptions = { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] } & Pick<LoaderOptions, 'secretKey'>; | ||
| type GetAuthOptions = { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] }; | ||
|
|
||
| export const getAuth: GetAuthFn<Request, true> = (async (request: Request, opts?: GetAuthOptions) => { | ||
| if (!request) { | ||
| return errorThrower.throw(noFetchFnCtxPassedInGetAuth); | ||
| } | ||
|
|
||
| const { acceptsToken, ...restOptions } = opts || {}; | ||
| const authObjectFn = getContext('auth'); | ||
|
|
||
| const loadedOptions = loadOptions(request, restOptions); | ||
|
|
||
| const requestState = await authenticateRequest(request, { | ||
| ...loadedOptions, | ||
| acceptsToken: 'any', | ||
| }); | ||
| if (!authObjectFn) { | ||
| return errorThrower.throw(clerkHandlerNotConfigured); | ||
| } | ||
|
|
||
| const authObject = requestState.toAuth(); | ||
| // We're keeping it a promise for now to minimize breaking changes | ||
| const authObject = await Promise.resolve(authObjectFn()); | ||
|
|
||
| return getAuthObjectForAcceptedToken({ authObject, acceptsToken }); | ||
| return getAuthObjectForAcceptedToken({ authObject, acceptsToken: opts?.acceptsToken }); | ||
| }) as GetAuthFn<Request, true>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,16 @@ | ||
| import { AuthStatus, constants } from '@clerk/backend/internal'; | ||
| import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler'; | ||
| import type { AnyRouter } from '@tanstack/react-router'; | ||
| import type { CustomizeStartHandler, HandlerCallback, RequestHandler } from '@tanstack/react-start/server'; | ||
| import { | ||
| type CustomizeStartHandler, | ||
| getEvent, | ||
| getWebRequest, | ||
| type HandlerCallback, | ||
| type RequestHandler, | ||
| } from '@tanstack/react-start/server'; | ||
|
|
||
| import { errorThrower } from '../utils'; | ||
| import { authenticateRequest } from './authenticateRequest'; | ||
| import { ClerkHandshakeRedirect } from './errors'; | ||
| import { loadOptions } from './loadOptions'; | ||
| import type { LoaderOptions } from './types'; | ||
| import { getResponseClerkState } from './utils'; | ||
|
|
@@ -11,41 +19,52 @@ export function createClerkHandler<TRouter extends AnyRouter>( | |
| eventHandler: CustomizeStartHandler<TRouter>, | ||
| clerkOptions: LoaderOptions = {}, | ||
| ) { | ||
| return (cb: HandlerCallback<TRouter>): RequestHandler => { | ||
| return eventHandler(async ({ request, router, responseHeaders }) => { | ||
| try { | ||
| const loadedOptions = loadOptions(request, clerkOptions); | ||
| return async (cb: HandlerCallback<TRouter>): Promise<RequestHandler> => { | ||
| const request = getWebRequest(); | ||
| const event = getEvent(); | ||
| const loadedOptions = loadOptions(request, clerkOptions); | ||
|
|
||
| const requestState = await authenticateRequest(request, { | ||
| ...loadedOptions, | ||
| acceptsToken: 'any', | ||
| }); | ||
| const requestState = await authenticateRequest(request, { | ||
| ...loadedOptions, | ||
| acceptsToken: 'any', | ||
| }); | ||
|
|
||
wobsoriano marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const { clerkInitialState, headers } = getResponseClerkState(requestState, loadedOptions); | ||
| // Set auth object here so it is available immediately in server functions via getAuth() | ||
| event.context.auth = () => requestState.toAuth(); | ||
|
Comment on lines
+27
to
+33
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved authentication logic outside of This change requires awaiting |
||
|
|
||
| // Merging the TanStack router context with the Clerk context and loading the router | ||
| router.update({ | ||
| context: { ...router.options.context, clerkInitialState }, | ||
| return eventHandler(async ({ request, router, responseHeaders }) => { | ||
| const locationHeader = requestState.headers.get(constants.Headers.Location); | ||
| if (locationHeader) { | ||
| handleNetlifyCacheInDevInstance({ | ||
| locationHeader, | ||
| requestStateHeaders: requestState.headers, | ||
| publishableKey: requestState.publishableKey, | ||
| }); | ||
|
|
||
| headers.forEach((value, key) => { | ||
| responseHeaders.set(key, value); | ||
| return new Response(null, { | ||
| status: 307, | ||
| headers: requestState.headers, | ||
| }); | ||
| } | ||
|
|
||
| await router.load(); | ||
| } catch (error) { | ||
| if (error instanceof ClerkHandshakeRedirect) { | ||
| // returning the response | ||
| return new Response(null, { | ||
| status: error.status, | ||
| headers: error.headers, | ||
| }); | ||
| } | ||
|
|
||
| // rethrowing the error if it is not a Response | ||
| throw error; | ||
| if (requestState.status === AuthStatus.Handshake) { | ||
| // eslint-disable-next-line @typescript-eslint/only-throw-error | ||
| throw errorThrower.throw('Clerk: unexpected handshake without redirect'); | ||
| } | ||
|
|
||
| const { clerkInitialState, headers } = getResponseClerkState(requestState, loadedOptions); | ||
|
|
||
| // Merging the TanStack router context with the Clerk context and loading the router | ||
| router.update({ | ||
| context: { ...router.options.context, clerkInitialState }, | ||
| }); | ||
|
|
||
| headers.forEach((value, key) => { | ||
| responseHeaders.set(key, value); | ||
| }); | ||
|
|
||
| await router.load(); | ||
|
|
||
| return cb({ request, router, responseHeaders }); | ||
| }); | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just doing a minor bump here since the SDK is still in beta