refresh token fix

This commit is contained in:
marko-kraemer 2025-07-25 20:04:00 +02:00
parent 159b128ecd
commit 293e97fa69
6 changed files with 212 additions and 206 deletions

View File

@ -1,28 +1,51 @@
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
import { checkAndInstallSunaAgent } from '@/lib/utils/install-suna-agent';
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function GET(request: Request) {
// The `/auth/callback` route is required for the server-side auth flow implemented
// by the SSR package. It exchanges an auth code for the user's session.
// https://supabase.com/docs/guides/auth/server-side/nextjs
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get('code');
const returnUrl = requestUrl.searchParams.get('returnUrl');
const origin = requestUrl.origin;
export async function GET(request: NextRequest) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/dashboard'
const error = searchParams.get('error')
const errorDescription = searchParams.get('error_description')
if (code) {
const supabase = await createClient();
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
await checkAndInstallSunaAgent(data.user.id, data.user.created_at);
console.log('🔵 Auth callback triggered:', {
hasCode: !!code,
next,
error,
errorDescription
})
if (error) {
console.error('❌ Auth callback error:', error, errorDescription)
return NextResponse.redirect(`${origin}/auth?error=${encodeURIComponent(error)}`)
}
// URL to redirect to after sign up process completes
// Handle the case where returnUrl is 'null' (string) or actual null
const redirectPath =
returnUrl && returnUrl !== 'null' ? returnUrl : '/dashboard';
// Make sure to include a slash between origin and path if needed
return NextResponse.redirect(
`${origin}${redirectPath.startsWith('/') ? '' : '/'}${redirectPath}`,
);
if (code) {
const supabase = await createClient()
try {
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
if (error) {
console.error('❌ Error exchanging code for session:', error)
return NextResponse.redirect(`${origin}/auth?error=${encodeURIComponent(error.message)}`)
}
console.log('✅ Successfully exchanged code for session:', {
userId: data.user?.id,
email: data.user?.email
})
// URL to redirect to after sign in process completes
return NextResponse.redirect(`${origin}${next}`)
} catch (error) {
console.error('❌ Unexpected error in auth callback:', error)
return NextResponse.redirect(`${origin}/auth?error=unexpected_error`)
}
}
// No code present, redirect to auth page
console.log('⚠️ No code present in auth callback, redirecting to auth')
return NextResponse.redirect(`${origin}/auth`)
}

View File

@ -30,37 +30,55 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
useEffect(() => {
const getInitialSession = async () => {
const {
data: { session: currentSession },
} = await supabase.auth.getSession();
setSession(currentSession);
setUser(currentSession?.user ?? null);
setIsLoading(false);
try {
const {
data: { session: currentSession },
} = await supabase.auth.getSession();
console.log('🔵 Initial session check:', { hasSession: !!currentSession, user: !!currentSession?.user });
setSession(currentSession);
setUser(currentSession?.user ?? null);
} catch (error) {
console.error('❌ Error getting initial session:', error);
} finally {
setIsLoading(false);
}
};
getInitialSession();
const { data: authListener } = supabase.auth.onAuthStateChange(
async (event, newSession) => {
console.log('🔵 Auth state change:', { event, session: !!newSession, user: !!newSession?.user });
console.log('🔵 Auth state change:', {
event,
hasSession: !!newSession,
hasUser: !!newSession?.user,
expiresAt: newSession?.expires_at
});
setSession(newSession);
// Only update user state on actual auth events, not token refresh
if (event === 'SIGNED_IN' || event === 'SIGNED_OUT') {
setUser(newSession?.user ?? null);
}
// For TOKEN_REFRESHED events, keep the existing user state
setUser(newSession?.user ?? null);
if (isLoading) setIsLoading(false);
if (event === 'SIGNED_IN' && newSession?.user) {
await checkAndInstallSunaAgent(newSession.user.id, newSession.user.created_at);
} else if (event === 'MFA_CHALLENGE_VERIFIED') {
console.log('✅ MFA challenge verified, session updated');
// Session is automatically updated by Supabase, just log for debugging
} else if (event === 'TOKEN_REFRESHED') {
console.log('🔄 Token refreshed, session updated');
// Handle specific auth events
switch (event) {
case 'SIGNED_IN':
if (newSession?.user) {
console.log('✅ User signed in');
await checkAndInstallSunaAgent(newSession.user.id, newSession.user.created_at);
}
break;
case 'SIGNED_OUT':
console.log('✅ User signed out');
break;
case 'TOKEN_REFRESHED':
console.log('🔄 Token refreshed successfully');
break;
case 'MFA_CHALLENGE_VERIFIED':
console.log('✅ MFA challenge verified');
break;
default:
console.log(`🔵 Auth event: ${event}`);
}
},
);
@ -68,11 +86,16 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
return () => {
authListener?.subscription.unsubscribe();
};
}, [supabase, isLoading]); // Added isLoading to dependencies to ensure it runs once after initial load completes
}, [supabase]); // Removed isLoading from dependencies to prevent infinite loops
const signOut = async () => {
await supabase.auth.signOut();
// State updates will be handled by onAuthStateChange
try {
console.log('🔵 Signing out...');
await supabase.auth.signOut();
// State updates will be handled by onAuthStateChange
} catch (error) {
console.error('❌ Error signing out:', error);
}
};
const value = {
@ -86,7 +109,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuth = (): AuthContextType => {
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');

View File

@ -1,24 +1,8 @@
import { createBrowserClient } from '@supabase/ssr';
import { createBrowserClient } from '@supabase/ssr'
export const createClient = () => {
// Get URL and key from environment variables
let supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
// Ensure the URL is in the proper format with http/https protocol
if (supabaseUrl && !supabaseUrl.startsWith('http')) {
// If it's just a hostname without protocol, add http://
supabaseUrl = `http://${supabaseUrl}`;
}
// console.log('Supabase URL:', supabaseUrl);
// console.log('Supabase Anon Key:', supabaseAnonKey);
return createBrowserClient(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true
}
});
};
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}

View File

@ -1,102 +0,0 @@
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { type NextRequest, NextResponse } from 'next/server';
function forceLoginWithReturn(request: NextRequest) {
const originalUrl = new URL(request.url);
const path = originalUrl.pathname;
const query = originalUrl.searchParams.toString();
return NextResponse.redirect(
new URL(
`/auth?returnUrl=${encodeURIComponent(path + (query ? `?${query}` : ''))}`,
request.url,
),
);
}
export const validateSession = async (request: NextRequest) => {
// This `try/catch` block is only here for the interactive tutorial.
// Feel free to remove once you have Supabase connected.
try {
// Create an unmodified response
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
// If the cookie is updated, update the cookies for the request and response
request.cookies.set({
name,
value,
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value,
...options,
});
},
remove(name: string, options: CookieOptions) {
// If the cookie is removed, update the cookies for the request and response
request.cookies.set({
name,
value: '',
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value: '',
...options,
});
},
},
},
);
// This will refresh session if expired - required for Server Components
// https://supabase.com/docs/guides/auth/server-side/nextjs
const {
data: { user },
} = await supabase.auth.getUser();
const protectedRoutes = ['/dashboard', '/invitation'];
if (
!user &&
protectedRoutes.some((path) => request.nextUrl.pathname.startsWith(path))
) {
// redirect to /auth
return forceLoginWithReturn(request);
}
return response;
} catch (e) {
// If you are here, a Supabase client could not be created!
// This is likely because you have not set up environment variables.
// Check out http://localhost:3000 for Next Steps.
return NextResponse.next({
request: {
headers: request.headers,
},
});
}
};

View File

@ -1,37 +1,30 @@
'use server';
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { cookies } from 'next/headers';
'use server'
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export const createClient = async () => {
const cookieStore = await cookies();
let supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export async function createClient() {
const cookieStore = await cookies()
// Ensure the URL is in the proper format with http/https protocol
if (supabaseUrl && !supabaseUrl.startsWith('http')) {
// If it's just a hostname without protocol, add http://
supabaseUrl = `http://${supabaseUrl}`;
}
// console.log('[SERVER] Supabase URL:', supabaseUrl);
// console.log('[SERVER] Supabase Anon Key:', supabaseAnonKey);
return createServerClient(supabaseUrl, supabaseAnonKey, {
cookies: {
getAll() {
return cookieStore.getAll();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
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.
}
},
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set({ name, value, ...options }),
);
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
});
};
}
)
}

View File

@ -0,0 +1,85 @@
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// IMPORTANT: DO NOT REMOVE auth.getUser()
// This refreshes the Auth token and is required for server-side auth
const {
data: { user },
} = await supabase.auth.getUser()
// Define protected routes
const protectedRoutes = ['/dashboard', '/agents', '/projects', '/settings', '/invitation']
const authRoutes = ['/auth', '/login', '/signup']
const isProtectedRoute = protectedRoutes.some(route =>
request.nextUrl.pathname.startsWith(route)
)
const isAuthRoute = authRoutes.some(route =>
request.nextUrl.pathname.startsWith(route)
)
// Redirect unauthenticated users from protected routes
if (isProtectedRoute && !user) {
const redirectUrl = new URL('/auth', request.url)
redirectUrl.searchParams.set('returnUrl', request.nextUrl.pathname)
return NextResponse.redirect(redirectUrl)
}
// Redirect authenticated users away from auth routes
if (isAuthRoute && user) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
// IMPORTANT: You *must* return the supabaseResponse object as it is.
// If you're creating a new response object with NextResponse.next() make sure to:
// 1. Pass the request in it, like so:
// const myNewResponse = NextResponse.next({ request })
// 2. Copy over the cookies, like so:
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
// 3. Change the myNewResponse object to fit your needs, but avoid changing
// the cookies!
// 4. Finally:
// return myNewResponse
// If this is not done, you may be causing the browser and server to go out
// of sync and terminate the user's session prematurely!
return supabaseResponse
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}