diff --git a/app/(auth)/auth/callback/route.ts b/app/(auth)/auth/callback/route.ts index 2f8959a1..f199e68c 100644 --- a/app/(auth)/auth/callback/route.ts +++ b/app/(auth)/auth/callback/route.ts @@ -1,35 +1,74 @@ -import { createClient } from '@/lib/supabase/server' -import { NextResponse } from 'next/server' +import { createServerClient } from '@supabase/ssr' +import { type NextRequest, NextResponse } from 'next/server' -export async function GET(request: Request) { +export async function GET(request: NextRequest) { const { searchParams, origin } = new URL(request.url) const code = searchParams.get('code') + const token_hash = searchParams.get('token_hash') + const type = searchParams.get('type') const next = searchParams.get('next') ?? '/' - if (code) { - const supabase = await createClient() - const { error } = await supabase.auth.exchangeCodeForSession(code) + // Collect cookies that Supabase sets during auth so we can + // explicitly forward them on the redirect response. + const pendingCookies: { name: string; value: string; options: Record }[] = [] - if (!error) { - // Check if user has completed onboarding - const { data: { user } } = await supabase.auth.getUser() - - if (user) { - const { data: settings } = await supabase - .from('company_settings') - .select('onboarding_complete') - .eq('user_id', user.id) - .single() - - if (!settings?.onboarding_complete) { - return NextResponse.redirect(`${origin}/onboarding`) - } - } - - return NextResponse.redirect(`${origin}${next}`) + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll() + }, + setAll(cookiesToSet) { + pendingCookies.length = 0 + cookiesToSet.forEach((cookie) => pendingCookies.push(cookie)) + }, + }, } + ) + + let authenticated = false + + // Handle PKCE flow (code exchange) + if (code) { + const { error } = await supabase.auth.exchangeCodeForSession(code) + authenticated = !error + } + // Handle token hash flow (email verification / magic link) + else if (token_hash && type) { + const { error } = await supabase.auth.verifyOtp({ + token_hash, + type: type as 'signup' | 'invite' | 'magiclink' | 'recovery' | 'email_change' | 'email', + }) + authenticated = !error } - // Return the user to an error page with instructions - return NextResponse.redirect(`${origin}/login?error=auth_error`) + if (authenticated) { + let redirectPath = next + + // Check if user has completed onboarding + const { data: { user } } = await supabase.auth.getUser() + if (user) { + const { data: settings } = await supabase + .from('company_settings') + .select('onboarding_complete') + .eq('user_id', user.id) + .single() + + if (!settings?.onboarding_complete) { + redirectPath = '/onboarding' + } + } + + // Create redirect and explicitly set auth cookies on the response + const response = NextResponse.redirect(new URL(redirectPath, origin)) + for (const { name, value, options } of pendingCookies) { + response.cookies.set({ name, value, ...options }) + } + return response + } + + // Authentication failed — redirect to login with error + return NextResponse.redirect(new URL('/login?error=auth_error', origin)) } diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 489f82d7..7da3b810 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -24,7 +24,7 @@ export default function LoginPage() { const { error } = await supabase.auth.signInWithOtp({ email, options: { - emailRedirectTo: `${process.env.NEXT_PUBLIC_APP_URL || window.location.origin}/auth/callback`, + emailRedirectTo: `${window.location.origin}/auth/callback`, }, })