diff --git a/apps/ui-library/app/example/password-based-auth/protected/page.tsx b/apps/ui-library/app/example/password-based-auth/protected/page.tsx index e711c3a3d21..9ab9b821c4c 100644 --- a/apps/ui-library/app/example/password-based-auth/protected/page.tsx +++ b/apps/ui-library/app/example/password-based-auth/protected/page.tsx @@ -1,7 +1,7 @@ import { redirect } from 'next/navigation' import { LogoutButton } from '@/registry/default/blocks/password-based-auth-nextjs/components/logout-button' -import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server' +import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server-component' export default async function ProtectedPage() { const supabase = await createClient() diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/app/auth/confirm/route.ts b/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/app/auth/confirm/route.ts index e2ac3a7fbe2..a49c80f8979 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/app/auth/confirm/route.ts +++ b/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/app/auth/confirm/route.ts @@ -1,32 +1,36 @@ import { type EmailOtpType } from '@supabase/supabase-js' -import { redirect } from 'next/navigation' -import { type NextRequest } from 'next/server' +import { NextResponse, type NextRequest } from 'next/server' -import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server' +import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server-route' export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url) + const { searchParams, origin } = new URL(request.url) const token_hash = searchParams.get('token_hash') const type = searchParams.get('type') as EmailOtpType | null const _next = searchParams.get('next') const next = _next?.startsWith('/') ? _next : '/' if (token_hash && type) { - const supabase = await createClient() + const { supabase, response } = createClient(request) const { error } = await supabase.auth.verifyOtp({ type, token_hash, }) if (!error) { - // redirect user to specified redirect URL or root of app - redirect(next) + const redirect = NextResponse.redirect(`${origin}${next}`) + redirect.cookies.setAll(response.cookies.getAll()) + response.headers.forEach((value, key) => redirect.headers.set(key, value)) + return redirect } else { - // redirect the user to an error page with some instructions - redirect(`/auth/error?error=${error?.message}`) + const redirect = NextResponse.redirect(`${origin}/auth/error?error=${error?.message}`) + redirect.cookies.setAll(response.cookies.getAll()) + response.headers.forEach((value, key) => redirect.headers.set(key, value)) + return redirect } } - // redirect the user to an error page with some instructions - redirect(`/auth/error?error=No token hash or type`) + return NextResponse.redirect( + `${new URL(request.url).origin}/auth/error?error=No token hash or type` + ) } diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/app/protected/page.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/app/protected/page.tsx index f84213f2771..6de095257c2 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/app/protected/page.tsx +++ b/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/app/protected/page.tsx @@ -1,7 +1,7 @@ import { redirect } from 'next/navigation' import { LogoutButton } from '@/registry/default/blocks/password-based-auth-nextjs/components/logout-button' -import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server' +import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server-component' export default async function ProtectedPage() { const supabase = await createClient() diff --git a/apps/ui-library/registry/default/blocks/social-auth-nextjs/app/auth/oauth/route.ts b/apps/ui-library/registry/default/blocks/social-auth-nextjs/app/auth/oauth/route.ts index ed032d1d2d5..373f7278bf0 100644 --- a/apps/ui-library/registry/default/blocks/social-auth-nextjs/app/auth/oauth/route.ts +++ b/apps/ui-library/registry/default/blocks/social-auth-nextjs/app/auth/oauth/route.ts @@ -1,9 +1,8 @@ -import { NextResponse } from 'next/server' +import { NextResponse, type NextRequest } from 'next/server' -// The client you created from the Server-Side Auth instructions -import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server' +import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server-route' -export async function GET(request: Request) { +export async function GET(request: NextRequest) { const { searchParams, origin } = new URL(request.url) const code = searchParams.get('code') // if "next" is in param, use it as the redirect URL @@ -14,22 +13,28 @@ export async function GET(request: Request) { } if (code) { - const supabase = await createClient() + const { supabase, response } = createClient(request) const { error } = await supabase.auth.exchangeCodeForSession(code) if (!error) { const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer const isLocalEnv = process.env.NODE_ENV === 'development' + + let redirectUrl: string if (isLocalEnv) { - // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host - return NextResponse.redirect(`${origin}${next}`) + redirectUrl = `${origin}${next}` } else if (forwardedHost) { - return NextResponse.redirect(`https://${forwardedHost}${next}`) + redirectUrl = `https://${forwardedHost}${next}` } else { - return NextResponse.redirect(`${origin}${next}`) + redirectUrl = `${origin}${next}` } + + const redirect = NextResponse.redirect(redirectUrl) + redirect.cookies.setAll(response.cookies.getAll()) + response.headers.forEach((value, key) => redirect.headers.set(key, value)) + return redirect } } // return the user to an error page with instructions - return NextResponse.redirect(`${origin}/auth/error`) + return NextResponse.redirect(`${new URL(request.url).origin}/auth/error`) } diff --git a/apps/ui-library/registry/default/blocks/social-auth-nextjs/app/protected/page.tsx b/apps/ui-library/registry/default/blocks/social-auth-nextjs/app/protected/page.tsx index 953458fde8b..e5896938580 100644 --- a/apps/ui-library/registry/default/blocks/social-auth-nextjs/app/protected/page.tsx +++ b/apps/ui-library/registry/default/blocks/social-auth-nextjs/app/protected/page.tsx @@ -1,7 +1,7 @@ import { redirect } from 'next/navigation' import { LogoutButton } from '@/registry/default/blocks/social-auth-nextjs/components/logout-button' -import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server' +import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server-component' export default async function ProtectedPage() { const supabase = await createClient() diff --git a/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server-component.ts b/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server-component.ts new file mode 100644 index 00000000000..a939b322aea --- /dev/null +++ b/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server-component.ts @@ -0,0 +1,24 @@ +import { createServerClient } from '@supabase/ssr' +import { cookies } from 'next/headers' + +/** + * Creates a read-only Supabase client for use in Server Components. + * This client can only read cookies (via getAll), not write them. + * For Route Handlers or Server Actions that need to set cookies + * and cache-control headers, use the client from './server-route' instead. + */ +export async function createClient() { + const cookieStore = await cookies() + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll() + }, + }, + } + ) +} diff --git a/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server-route.ts b/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server-route.ts new file mode 100644 index 00000000000..2c9e2ae950e --- /dev/null +++ b/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server-route.ts @@ -0,0 +1,34 @@ +import { createServerClient } from '@supabase/ssr' +import { NextResponse, type NextRequest } from 'next/server' + +/** + * Creates a Supabase client for use in Route Handlers and Server Actions. + * This client can read and write cookies, and applies cache-control headers + * to prevent CDNs from caching authenticated responses. + * For Server Components (read-only), use the client from './server-component' instead. + */ +export function createClient(request: NextRequest) { + let response = NextResponse.next({ request }) + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll() + }, + setAll(cookiesToSet, headers) { + cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value)) + response = NextResponse.next({ request }) + cookiesToSet.forEach(({ name, value, options }) => + response.cookies.set(name, value, options) + ) + Object.entries(headers).forEach(([key, value]) => response.headers.set(key, value)) + }, + }, + } + ) + + return { supabase, response } +} diff --git a/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server.ts b/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server.ts deleted file mode 100644 index ba88fa32deb..00000000000 --- a/apps/ui-library/registry/default/clients/nextjs/lib/supabase/server.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createServerClient } from '@supabase/ssr' -import { cookies } from 'next/headers' - -/** - * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each - * function when using it. - */ -export async function createClient() { - const cookieStore = await cookies() - - return createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, - { - cookies: { - getAll() { - return cookieStore.getAll() - }, - setAll(cookiesToSet, _headers) { - try { - cookiesToSet.forEach(({ name, value, options }) => - cookieStore.set(name, value, options) - ) - } catch { - // The `setAll` method was called from a Server Component. - // This can be ignored if you have middleware refreshing - // user sessions. - } - }, - }, - } - ) -} diff --git a/apps/ui-library/registry/default/clients/nextjs/registry-item.json b/apps/ui-library/registry/default/clients/nextjs/registry-item.json index 14af3620e5a..9978f4b7190 100644 --- a/apps/ui-library/registry/default/clients/nextjs/registry-item.json +++ b/apps/ui-library/registry/default/clients/nextjs/registry-item.json @@ -21,7 +21,11 @@ "type": "registry:lib" }, { - "path": "registry/default/clients/nextjs/lib/supabase/server.ts", + "path": "registry/default/clients/nextjs/lib/supabase/server-component.ts", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/server-route.ts", "type": "registry:lib" } ]