mirror of https://github.com/kortix-ai/suna.git
refresh token fix
This commit is contained in:
parent
159b128ecd
commit
293e97fa69
|
@ -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`)
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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!
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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.
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)$).*)',
|
||||
],
|
||||
}
|
Loading…
Reference in New Issue