diff --git a/examples/react/start-workos/package.json b/examples/react/start-workos/package.json index 6b49f77eef7..13d59fa3b24 100644 --- a/examples/react/start-workos/package.json +++ b/examples/react/start-workos/package.json @@ -18,6 +18,7 @@ "@tanstack/react-router-devtools": "^1.132.47", "@tanstack/react-start": "^1.132.47", "@workos-inc/node": "^7.45.0", + "@workos/authkit-tanstack-react-start": "^0.1.0", "iron-session": "^8.0.4", "jose": "^6.0.10", "react": "^19.0.0", @@ -27,8 +28,8 @@ "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", - "postcss": "^8.5.1", "autoprefixer": "^10.4.20", + "postcss": "^8.5.1", "tailwindcss": "^3.4.17", "typescript": "^5.7.2", "vite": "^7.1.7", diff --git a/examples/react/start-workos/src/app.css b/examples/react/start-workos/src/app.css new file mode 100644 index 00000000000..e544efd1a38 --- /dev/null +++ b/examples/react/start-workos/src/app.css @@ -0,0 +1 @@ +@import '@radix-ui/themes/styles.css'; diff --git a/examples/react/start-workos/src/authkit/serverFunctions.ts b/examples/react/start-workos/src/authkit/serverFunctions.ts deleted file mode 100644 index 07d5909f8bd..00000000000 --- a/examples/react/start-workos/src/authkit/serverFunctions.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { createServerFn } from '@tanstack/react-start'; -import { deleteCookie } from '@tanstack/react-start/server'; -import { getConfig } from './ssr/config'; -import { terminateSession, withAuth } from './ssr/session'; -import { getWorkOS } from './ssr/workos'; -import type { GetAuthURLOptions, NoUserInfo, UserInfo } from './ssr/interfaces'; - -export const getAuthorizationUrl = createServerFn({ method: 'GET' }) - .inputValidator((options?: GetAuthURLOptions) => options) - .handler(({ data: options = {} }) => { - const { returnPathname, screenHint, redirectUri } = options; - - return getWorkOS().userManagement.getAuthorizationUrl({ - provider: 'authkit', - clientId: getConfig('clientId'), - redirectUri: redirectUri || getConfig('redirectUri'), - state: returnPathname ? btoa(JSON.stringify({ returnPathname })) : undefined, - screenHint, - }); - }); - -export const getSignInUrl = createServerFn({ method: 'GET' }) - .inputValidator((data?: string) => data) - .handler(async ({ data: returnPathname }) => { - return await getAuthorizationUrl({ data: { returnPathname, screenHint: 'sign-in' } }); - }); - -export const getSignUpUrl = createServerFn({ method: 'GET' }) - .inputValidator((data?: string) => data) - .handler(async ({ data: returnPathname }) => { - return getAuthorizationUrl({ data: { returnPathname, screenHint: 'sign-up' } }); - }); - -export const signOut = createServerFn({ method: 'POST' }) - .inputValidator((data?: string) => data) - .handler(async ({ data: returnTo }) => { - const cookieName = getConfig('cookieName') || 'wos_session'; - deleteCookie(cookieName); - await terminateSession({ returnTo }); - }); - -export const getAuth = createServerFn({ method: 'GET' }).handler(async (): Promise => { - const auth = await withAuth(); - return auth; -}); diff --git a/examples/react/start-workos/src/authkit/ssr/config.ts b/examples/react/start-workos/src/authkit/ssr/config.ts deleted file mode 100644 index e815e54cea8..00000000000 --- a/examples/react/start-workos/src/authkit/ssr/config.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { lazy } from './utils.js'; -import type { AuthKitConfig } from './interfaces.js'; - -type ValueSource = Record | ((key: string) => any); - -/** - * Default environment variable source that uses process.env - */ -const defaultSource: ValueSource = (key: string): string | undefined => { - try { - return process.env[key]; - } catch { - return undefined; - } -}; - -/** - * Configuration class for AuthKit. - * This class is used to manage configuration values and provide defaults. - * It also provides a way to get configuration values from environment variables. - * @internal - */ -export class Configuration { - private config: Partial = { - cookieName: 'wos-session', - apiHttps: true, - // Defaults to 400 days, the maximum allowed by Chrome - // It's fine to have a long cookie expiry date as the access/refresh tokens - // act as the actual time-limited aspects of the session. - cookieMaxAge: 60 * 60 * 24 * 400, - apiHostname: 'api.workos.com', - }; - - private valueSource: ValueSource = defaultSource; - - private readonly requiredKeys: Array = ['clientId', 'apiKey', 'redirectUri', 'cookiePassword']; - - /** - * Convert a camelCase string to an uppercase, underscore-separated environment variable name. - * @param str The string to convert - * @returns The environment variable name - */ - protected getEnvironmentVariableName(str: string) { - return `WORKOS_${str.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()}`; - } - - private updateConfig(config: Partial): void { - this.config = { ...this.config, ...config }; - } - - setValueSource(source: ValueSource): void { - this.valueSource = source; - } - - configure(configOrSource: Partial | ValueSource, source?: ValueSource): void { - if (typeof configOrSource === 'function') { - this.setValueSource(configOrSource); - } else if (typeof configOrSource === 'object' && !source) { - this.updateConfig(configOrSource); - } else if (typeof configOrSource === 'object' && source) { - this.updateConfig(configOrSource); - this.setValueSource(source); - } - - // Validate the cookiePassword if provided - if (this.config.cookiePassword && this.config.cookiePassword.length < 32) { - throw new Error('cookiePassword must be at least 32 characters long'); - } - } - - getValue(key: T): AuthKitConfig[T] { - // First check environment variables - const envKey = this.getEnvironmentVariableName(key); - let envValue: AuthKitConfig[T] | undefined = undefined; - - const { valueSource, config } = this; - if (typeof valueSource === 'function') { - envValue = valueSource(envKey); - } else { - envValue = valueSource[envKey]; - } - - // If environment variable exists, use it - if (envValue != null) { - // Convert string values to appropriate types - if (key === 'apiHttps' && typeof envValue === 'string') { - return (envValue === 'true') as AuthKitConfig[T]; - } - - if ((key === 'apiPort' || key === 'cookieMaxAge') && typeof envValue === 'string') { - const num = parseInt(envValue, 10); - return (isNaN(num) ? undefined : num) as AuthKitConfig[T]; - } - - return envValue as AuthKitConfig[T]; - } - - // Then check programmatically provided config - if (key in config && config[key] != undefined) { - return config[key] as AuthKitConfig[T]; - } - - if (this.requiredKeys.includes(key)) { - throw new Error(`Missing required configuration value for ${key} (${envKey}).`); - } - - return undefined as AuthKitConfig[T]; - } -} - -// lazy-instantiate the Configuration instance -const getConfigurationInstance = lazy(() => new Configuration()); - -/** - * Configure AuthKit with a custom value source. - * @param source The source of configuration values - * - * @example - * configure(key => Deno.env.get(key)); - */ -export function configure(source: ValueSource): void; -/** - * Configure AuthKit with custom values. - * @param config The configuration values - * - * @example - * configure({ - * clientId: 'your-client-id', - * redirectUri: 'https://your-app.com/auth/callback', - * apiKey: 'your-api-key', - * cookiePassword: 'your-cookie-password', - * }); - */ -export function configure(config: Partial): void; -/** - * Configure AuthKit with custom values and a custom value source. - * @param config The configuration values - * @param source The source of configuration values - * - * @example - * configure({ - * clientId: 'your-client-id', - * }, env); - */ -export function configure(config: Partial, source: ValueSource): void; -export function configure(configOrSource: Partial | ValueSource, source?: ValueSource): void { - const config = getConfigurationInstance(); - config.configure(configOrSource, source); -} - -/** - * Get a configuration value by key. - * This function will first check environment variables, then programmatically provided config, - * and finally fall back to defaults for optional settings. - * If a required setting is missing, an error will be thrown. - * @param key The configuration key - * @returns The configuration value - */ -export function getConfig(key: T): AuthKitConfig[T] { - const config = getConfigurationInstance(); - return config.getValue(key); -} diff --git a/examples/react/start-workos/src/authkit/ssr/interfaces.ts b/examples/react/start-workos/src/authkit/ssr/interfaces.ts deleted file mode 100644 index bd17a17d56b..00000000000 --- a/examples/react/start-workos/src/authkit/ssr/interfaces.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { Impersonator, User } from '@workos-inc/node'; - -export interface GetAuthURLOptions { - redirectUri?: string; - screenHint?: 'sign-up' | 'sign-in'; - returnPathname?: string; -} - -export interface CookieOptions { - path: '/'; - httpOnly: true; - secure: boolean; - sameSite: 'lax' | 'strict' | 'none'; - maxAge: number; - domain: string | undefined; -} - -export interface UserInfo { - user: User; - sessionId: string; - organizationId?: string; - role?: string; - permissions?: Array; - entitlements?: Array; - impersonator?: Impersonator; - accessToken: string; -} -export interface NoUserInfo { - user: null; - sessionId?: undefined; - organizationId?: undefined; - role?: undefined; - permissions?: undefined; - entitlements?: undefined; - impersonator?: undefined; - accessToken?: undefined; -} - -export interface AuthkitOptions { - debug?: boolean; - redirectUri?: string; - screenHint?: 'sign-up' | 'sign-in'; -} - -export interface AuthkitResponse { - session: UserInfo | NoUserInfo; - headers: Headers; - authorizationUrl?: string; -} - -/** - * AuthKit Session - */ -export interface Session { - /** - * The session access token - */ - accessToken: string; - /** - * The session refresh token - used to refresh the access token - */ - refreshToken: string; - /** - * The logged-in user - */ - user: User; - /** - * The impersonator user, if any - */ - impersonator?: Impersonator; -} - -/** - * AuthKit Configuration Options - */ -export interface AuthKitConfig { - /** - * The WorkOS Client ID - * Equivalent to the WORKOS_CLIENT_ID environment variable - */ - clientId: string; - - /** - * The WorkOS API Key - * Equivalent to the WORKOS_API_KEY environment variable - */ - apiKey: string; - - /** - * The redirect URI for the authentication callback - * Equivalent to the WORKOS_REDIRECT_URI environment variable - */ - redirectUri: string; - - /** - * The password used to encrypt the session cookie - * Equivalent to the WORKOS_COOKIE_PASSWORD environment variable - * Must be at least 32 characters long - */ - cookiePassword: string; - - /** - * The hostname of the API to use - * Equivalent to the WORKOS_API_HOSTNAME environment variable - */ - apiHostname?: string; - - /** - * Whether to use HTTPS for API requests - * Equivalent to the WORKOS_API_HTTPS environment variable - */ - apiHttps: boolean; - - /** - * The port to use for the API - * Equivalent to the WORKOS_API_PORT environment variable - */ - apiPort?: number; - - /** - * The maximum age of the session cookie in seconds - * Equivalent to the WORKOS_COOKIE_MAX_AGE environment variable - */ - cookieMaxAge: number; - - /** - * The name of the session cookie - * Equivalent to the WORKOS_COOKIE_NAME environment variable - * Defaults to "wos-session" - */ - cookieName: string; - - /** - * The domain for the session cookie - */ - cookieDomain?: string; -} diff --git a/examples/react/start-workos/src/authkit/ssr/session.ts b/examples/react/start-workos/src/authkit/ssr/session.ts deleted file mode 100644 index 1c58b6736d7..00000000000 --- a/examples/react/start-workos/src/authkit/ssr/session.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { redirect } from '@tanstack/react-router'; -import { getCookie, setCookie } from '@tanstack/react-start/server'; -import { sealData, unsealData } from 'iron-session'; -import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose'; -import { getConfig } from './config'; -import { lazy } from './utils'; -import { getWorkOS } from './workos'; -import type { AccessToken, AuthenticationResponse } from '@workos-inc/node'; -import type { AuthkitOptions, AuthkitResponse, CookieOptions, GetAuthURLOptions, Session } from './interfaces'; - -const sessionHeaderName = 'x-workos-session'; -const middlewareHeaderName = 'x-workos-middleware'; - -export function getAuthorizationUrl(options: GetAuthURLOptions = {}) { - const { returnPathname, screenHint, redirectUri } = options; - - return getWorkOS().userManagement.getAuthorizationUrl({ - provider: 'authkit', - clientId: getConfig('clientId'), - redirectUri: redirectUri || getConfig('redirectUri'), - state: returnPathname ? btoa(JSON.stringify({ returnPathname })) : undefined, - screenHint, - }); -} - -export function serializeCookie(name: string, value: string, options: Partial = {}): string { - const { - path = '/', - maxAge = getConfig('cookieMaxAge'), - secure = options.sameSite === 'none' ? true : getConfig('redirectUri').startsWith('https:'), - sameSite = 'lax', - domain = getConfig('cookieDomain'), - } = options; - - let cookie = `${name}=${encodeURIComponent(value)}; Path=${path}; sameSite=${sameSite}; HttpOnly`; - cookie += `; Max-Age=${maxAge}`; - if (!maxAge) cookie += `; Expires=${new Date(0).toUTCString()}`; - if (secure) cookie += '; Secure'; - if (domain) cookie += `; Domain=${domain}`; - - return cookie; -} - -export async function decryptSession(encryptedSession: string): Promise { - const cookiePassword = getConfig('cookiePassword'); - return unsealData(encryptedSession, { - password: cookiePassword, - }); -} - -export async function encryptSession(session: Session) { - return sealData(session, { - password: getConfig('cookiePassword'), - ttl: 0, - }); -} - -export async function withAuth() { - const session = await getSessionFromCookie(); - - if (!session?.user) { - return { user: null }; - } - - const { - sid: sessionId, - org_id: organizationId, - role, - permissions, - entitlements, - } = decodeJwt(session.accessToken); - - return { - sessionId, - user: session.user, - organizationId, - role, - permissions, - entitlements, - impersonator: session.impersonator, - accessToken: session.accessToken, - }; -} - -export async function getSessionFromCookie() { - const cookieName = getConfig('cookieName') || 'wos-session'; - const cookie = getCookie(cookieName); - - if (cookie) { - return decryptSession(cookie); - } -} - -export async function saveSession(sessionOrResponse: Session | AuthenticationResponse): Promise { - const cookieName = getConfig('cookieName') || 'wos-session'; - const encryptedSession = await encryptSession(sessionOrResponse); - setCookie(cookieName, encryptedSession); -} - -// JWKS call only happens once and the result is cached. The lazy function ensures that -// the JWK set is only created when it's needed, and not before. -const JWKS = lazy(() => createRemoteJWKSet(new URL(getWorkOS().userManagement.getJwksUrl(getConfig('clientId'))))); - -async function verifyAccessToken(accessToken: string): Promise { - try { - await jwtVerify(accessToken, JWKS()); - return true; - } catch { - return false; - } -} - -function getReturnPathname(url: string): string { - const newUrl = new URL(url); - - return `${newUrl.pathname}${newUrl.searchParams.size > 0 ? '?' + newUrl.searchParams.toString() : ''}`; -} - -export async function updateSession( - request: Request, - options: AuthkitOptions = { debug: false }, -): Promise { - const session = await getSessionFromCookie(); - - const newRequestHeaders = new Headers(); - - // Record that the request was routed through the middleware so we can check later for DX purposes - newRequestHeaders.set(middlewareHeaderName, 'true'); - - // We store the current request url in a custom header, so we can always have access to it - // This is because on hard navigations we don't have access to `next-url` but need to get the current - // `pathname` to be able to return the users where they came from before sign-in - newRequestHeaders.set('x-url', request.url); - - if (options.redirectUri) { - // Store the redirect URI in a custom header, so we always have access to it and so that subsequent - // calls to `getAuthorizationUrl` will use the same redirect URI - newRequestHeaders.set('x-redirect-uri', options.redirectUri); - } - - newRequestHeaders.delete(sessionHeaderName); - - if (!session) { - if (options.debug) { - console.log('No session found from cookie'); - } - - return { - session: { user: null }, - headers: newRequestHeaders, - authorizationUrl: getAuthorizationUrl({ - returnPathname: getReturnPathname(request.url), - redirectUri: options.redirectUri || getConfig('redirectUri'), - screenHint: options.screenHint, - }), - }; - } - - const hasValidSession = await verifyAccessToken(session.accessToken); - - const cookieName = getConfig('cookieName') || 'wos-session'; - - if (hasValidSession) { - newRequestHeaders.set(sessionHeaderName, getCookie(cookieName)!); - - const { - sid: sessionId, - org_id: organizationId, - role, - permissions, - entitlements, - } = decodeJwt(session.accessToken); - - return { - session: { - sessionId, - user: session.user, - organizationId, - role, - permissions, - entitlements, - impersonator: session.impersonator, - accessToken: session.accessToken, - }, - headers: newRequestHeaders, - }; - } - - try { - if (options.debug) { - // istanbul ignore next - console.log( - `Session invalid. ${session.accessToken ? `Refreshing access token that ends in ${session.accessToken.slice(-10)}` : 'Access token missing.'}`, - ); - } - - const { org_id: organizationIdFromAccessToken } = decodeJwt(session.accessToken); - - const { accessToken, refreshToken, user, impersonator } = - await getWorkOS().userManagement.authenticateWithRefreshToken({ - clientId: getConfig('clientId'), - refreshToken: session.refreshToken, - organizationId: organizationIdFromAccessToken, - }); - - if (options.debug) { - console.log('Session successfully refreshed'); - } - // Encrypt session with new access and refresh tokens - const encryptedSession = await encryptSession({ - accessToken, - refreshToken, - user, - impersonator, - }); - - newRequestHeaders.append('Set-Cookie', serializeCookie(cookieName, encryptedSession)); - newRequestHeaders.set(sessionHeaderName, encryptedSession); - - const { - sid: sessionId, - org_id: organizationId, - role, - permissions, - entitlements, - } = decodeJwt(accessToken); - - return { - session: { - sessionId, - user, - organizationId, - role, - permissions, - entitlements, - impersonator, - accessToken, - }, - headers: newRequestHeaders, - }; - } catch (e) { - if (options.debug) { - console.log('Failed to refresh. Deleting cookie.', e); - } - - // When we need to delete a cookie, return it as a header as you can't delete cookies from edge middleware - const deleteCookie = serializeCookie(cookieName, '', { maxAge: 0 }); - newRequestHeaders.append('Set-Cookie', deleteCookie); - - return { - session: { user: null }, - headers: newRequestHeaders, - authorizationUrl: getAuthorizationUrl({ - returnPathname: getReturnPathname(request.url), - }), - }; - } -} - -export async function terminateSession({ returnTo }: { returnTo?: string } = {}) { - const { sessionId } = await withAuth(); - if (sessionId) { - const href = getWorkOS().userManagement.getLogoutUrl({ sessionId, returnTo }); - return redirect({ href, throw: true, reloadDocument: true }); - } - - return redirect({ to: returnTo ?? '/', throw: true, reloadDocument: true }); -} diff --git a/examples/react/start-workos/src/authkit/ssr/utils.ts b/examples/react/start-workos/src/authkit/ssr/utils.ts deleted file mode 100644 index 73b807cb568..00000000000 --- a/examples/react/start-workos/src/authkit/ssr/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Returns a function that can only be called once. - * Subsequent calls will return the result of the first call. - * This is useful for lazy initialization. - * @param fn - The function to be called once. - * @returns A function that can only be called once. - */ -export function lazy(fn: () => T): () => T { - let called = false; - let result: T; - return () => { - if (!called) { - result = fn(); - called = true; - } - return result; - }; -} diff --git a/examples/react/start-workos/src/authkit/ssr/workos.ts b/examples/react/start-workos/src/authkit/ssr/workos.ts deleted file mode 100644 index b3107992105..00000000000 --- a/examples/react/start-workos/src/authkit/ssr/workos.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { WorkOS } from '@workos-inc/node'; -import { getConfig } from './config'; -import { lazy } from './utils'; - -/** - * Create a WorkOS instance with the provided API key and optional settings. - */ -export function createWorkOSInstance() { - // Get required API key from config - const apiKey = getConfig('apiKey'); - - // Get optional settings - const apiHostname = getConfig('apiHostname'); - const apiHttps = getConfig('apiHttps'); - const apiPort = getConfig('apiPort'); - - const options = { - apiHostname, - https: apiHttps, - port: apiPort, - }; - - // Initialize the WorkOS client with config values - const workos = new WorkOS(apiKey, options); - - return workos; -} - -/** - * Create a WorkOS instance with the provided API key and optional settings. - * This function is lazy loaded to avoid loading the WorkOS SDK when it's not needed. - */ -export const getWorkOS = lazy(createWorkOSInstance); diff --git a/examples/react/start-workos/src/routeTree.gen.ts b/examples/react/start-workos/src/routeTree.gen.ts index 8e275a0f1b6..f035b5ac448 100644 --- a/examples/react/start-workos/src/routeTree.gen.ts +++ b/examples/react/start-workos/src/routeTree.gen.ts @@ -10,6 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as LogoutRouteImport } from './routes/logout' +import { Route as ClientRouteImport } from './routes/client' import { Route as AuthenticatedRouteImport } from './routes/_authenticated' import { Route as IndexRouteImport } from './routes/index' import { Route as AuthenticatedAccountRouteImport } from './routes/_authenticated/account' @@ -20,6 +21,11 @@ const LogoutRoute = LogoutRouteImport.update({ path: '/logout', getParentRoute: () => rootRouteImport, } as any) +const ClientRoute = ClientRouteImport.update({ + id: '/client', + path: '/client', + getParentRoute: () => rootRouteImport, +} as any) const AuthenticatedRoute = AuthenticatedRouteImport.update({ id: '/_authenticated', getParentRoute: () => rootRouteImport, @@ -42,12 +48,14 @@ const ApiAuthCallbackRoute = ApiAuthCallbackRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/client': typeof ClientRoute '/logout': typeof LogoutRoute '/account': typeof AuthenticatedAccountRoute '/api/auth/callback': typeof ApiAuthCallbackRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/client': typeof ClientRoute '/logout': typeof LogoutRoute '/account': typeof AuthenticatedAccountRoute '/api/auth/callback': typeof ApiAuthCallbackRoute @@ -56,19 +64,21 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/_authenticated': typeof AuthenticatedRouteWithChildren + '/client': typeof ClientRoute '/logout': typeof LogoutRoute '/_authenticated/account': typeof AuthenticatedAccountRoute '/api/auth/callback': typeof ApiAuthCallbackRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/logout' | '/account' | '/api/auth/callback' + fullPaths: '/' | '/client' | '/logout' | '/account' | '/api/auth/callback' fileRoutesByTo: FileRoutesByTo - to: '/' | '/logout' | '/account' | '/api/auth/callback' + to: '/' | '/client' | '/logout' | '/account' | '/api/auth/callback' id: | '__root__' | '/' | '/_authenticated' + | '/client' | '/logout' | '/_authenticated/account' | '/api/auth/callback' @@ -77,6 +87,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute AuthenticatedRoute: typeof AuthenticatedRouteWithChildren + ClientRoute: typeof ClientRoute LogoutRoute: typeof LogoutRoute ApiAuthCallbackRoute: typeof ApiAuthCallbackRoute } @@ -90,6 +101,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LogoutRouteImport parentRoute: typeof rootRouteImport } + '/client': { + id: '/client' + path: '/client' + fullPath: '/client' + preLoaderRoute: typeof ClientRouteImport + parentRoute: typeof rootRouteImport + } '/_authenticated': { id: '/_authenticated' path: '' @@ -136,6 +154,7 @@ const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AuthenticatedRoute: AuthenticatedRouteWithChildren, + ClientRoute: ClientRoute, LogoutRoute: LogoutRoute, ApiAuthCallbackRoute: ApiAuthCallbackRoute, } @@ -144,10 +163,11 @@ export const routeTree = rootRouteImport ._addFileTypes() import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' +import type { startInstance } from './start.ts' declare module '@tanstack/react-start' { interface Register { ssr: true router: Awaited> + config: Awaited> } } diff --git a/examples/react/start-workos/src/routes/__root.tsx b/examples/react/start-workos/src/routes/__root.tsx index 8cdf0bd5bc8..d7da0c406d3 100644 --- a/examples/react/start-workos/src/routes/__root.tsx +++ b/examples/react/start-workos/src/routes/__root.tsx @@ -1,19 +1,15 @@ import { Box, Button, Card, Container, Flex, Theme } from '@radix-ui/themes'; -import '@radix-ui/themes/styles.css'; import { HeadContent, Link, Outlet, Scripts, createRootRoute } from '@tanstack/react-router'; import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'; import { Suspense } from 'react'; -import { getAuth, getSignInUrl } from '../authkit/serverFunctions'; +import { getAuth, getSignInUrl } from '@workos/authkit-tanstack-react-start'; +import { AuthKitProvider } from '@workos/authkit-tanstack-react-start/client'; import Footer from '../components/footer'; import SignInButton from '../components/sign-in-button'; import type { ReactNode } from 'react'; +import appCssUrl from '../app.css?url'; export const Route = createRootRoute({ - beforeLoad: async () => { - const { user } = await getAuth(); - - return { user }; - }, head: () => ({ meta: [ { @@ -27,9 +23,11 @@ export const Route = createRootRoute({ title: 'AuthKit Example in TanStack Start', }, ], + links: [{ rel: 'stylesheet', href: appCssUrl }], }), - loader: async ({ context }) => { - const { user } = context; + loader: async () => { + // getAuth() is a server function - works during client-side navigation + const { user } = await getAuth(); const url = await getSignInUrl(); return { user, @@ -44,50 +42,56 @@ function RootComponent() { const { user, url } = Route.useLoaderData(); return ( - - - - - - - -
- - + + + + + + + + +
+ + - - + - Loading...}> - - -
-
+ +
+ + Loading...}> + + +
+
- -
- -
+ +
+ +
+
-
-
-
-