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 { createClient } from '@/lib/supabase/server'
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server'
|
||||||
import { checkAndInstallSunaAgent } from '@/lib/utils/install-suna-agent';
|
import type { NextRequest } from 'next/server'
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: NextRequest) {
|
||||||
// The `/auth/callback` route is required for the server-side auth flow implemented
|
const { searchParams, origin } = new URL(request.url)
|
||||||
// by the SSR package. It exchanges an auth code for the user's session.
|
const code = searchParams.get('code')
|
||||||
// https://supabase.com/docs/guides/auth/server-side/nextjs
|
const next = searchParams.get('next') ?? '/dashboard'
|
||||||
const requestUrl = new URL(request.url);
|
const error = searchParams.get('error')
|
||||||
const code = requestUrl.searchParams.get('code');
|
const errorDescription = searchParams.get('error_description')
|
||||||
const returnUrl = requestUrl.searchParams.get('returnUrl');
|
|
||||||
const origin = requestUrl.origin;
|
|
||||||
|
|
||||||
if (code) {
|
console.log('🔵 Auth callback triggered:', {
|
||||||
const supabase = await createClient();
|
hasCode: !!code,
|
||||||
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
|
next,
|
||||||
await checkAndInstallSunaAgent(data.user.id, data.user.created_at);
|
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
|
if (code) {
|
||||||
// Handle the case where returnUrl is 'null' (string) or actual null
|
const supabase = await createClient()
|
||||||
const redirectPath =
|
|
||||||
returnUrl && returnUrl !== 'null' ? returnUrl : '/dashboard';
|
try {
|
||||||
// Make sure to include a slash between origin and path if needed
|
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
|
||||||
return NextResponse.redirect(
|
|
||||||
`${origin}${redirectPath.startsWith('/') ? '' : '/'}${redirectPath}`,
|
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(() => {
|
useEffect(() => {
|
||||||
const getInitialSession = async () => {
|
const getInitialSession = async () => {
|
||||||
const {
|
try {
|
||||||
data: { session: currentSession },
|
const {
|
||||||
} = await supabase.auth.getSession();
|
data: { session: currentSession },
|
||||||
setSession(currentSession);
|
} = await supabase.auth.getSession();
|
||||||
setUser(currentSession?.user ?? null);
|
console.log('🔵 Initial session check:', { hasSession: !!currentSession, user: !!currentSession?.user });
|
||||||
setIsLoading(false);
|
setSession(currentSession);
|
||||||
|
setUser(currentSession?.user ?? null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error getting initial session:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getInitialSession();
|
getInitialSession();
|
||||||
|
|
||||||
const { data: authListener } = supabase.auth.onAuthStateChange(
|
const { data: authListener } = supabase.auth.onAuthStateChange(
|
||||||
async (event, newSession) => {
|
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);
|
setSession(newSession);
|
||||||
|
setUser(newSession?.user ?? null);
|
||||||
// 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
|
|
||||||
|
|
||||||
if (isLoading) setIsLoading(false);
|
if (isLoading) setIsLoading(false);
|
||||||
|
|
||||||
if (event === 'SIGNED_IN' && newSession?.user) {
|
// Handle specific auth events
|
||||||
await checkAndInstallSunaAgent(newSession.user.id, newSession.user.created_at);
|
switch (event) {
|
||||||
} else if (event === 'MFA_CHALLENGE_VERIFIED') {
|
case 'SIGNED_IN':
|
||||||
console.log('✅ MFA challenge verified, session updated');
|
if (newSession?.user) {
|
||||||
// Session is automatically updated by Supabase, just log for debugging
|
console.log('✅ User signed in');
|
||||||
} else if (event === 'TOKEN_REFRESHED') {
|
await checkAndInstallSunaAgent(newSession.user.id, newSession.user.created_at);
|
||||||
console.log('🔄 Token refreshed, session updated');
|
}
|
||||||
|
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 () => {
|
return () => {
|
||||||
authListener?.subscription.unsubscribe();
|
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 () => {
|
const signOut = async () => {
|
||||||
await supabase.auth.signOut();
|
try {
|
||||||
// State updates will be handled by onAuthStateChange
|
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 = {
|
const value = {
|
||||||
|
@ -86,7 +109,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAuth = (): AuthContextType => {
|
export const useAuth = () => {
|
||||||
const context = useContext(AuthContext);
|
const context = useContext(AuthContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error('useAuth must be used within an AuthProvider');
|
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 = () => {
|
export function createClient() {
|
||||||
// Get URL and key from environment variables
|
return createBrowserClient(
|
||||||
let supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -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';
|
'use server'
|
||||||
import { createServerClient, type CookieOptions } from '@supabase/ssr';
|
import { createServerClient } from '@supabase/ssr'
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers'
|
||||||
|
|
||||||
export const createClient = async () => {
|
export async function createClient() {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies()
|
||||||
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
|
return createServerClient(
|
||||||
if (supabaseUrl && !supabaseUrl.startsWith('http')) {
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
// If it's just a hostname without protocol, add http://
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
supabaseUrl = `http://${supabaseUrl}`;
|
{
|
||||||
}
|
cookies: {
|
||||||
|
getAll() {
|
||||||
// console.log('[SERVER] Supabase URL:', supabaseUrl);
|
return cookieStore.getAll()
|
||||||
// console.log('[SERVER] Supabase Anon Key:', supabaseAnonKey);
|
},
|
||||||
|
setAll(cookiesToSet) {
|
||||||
return createServerClient(supabaseUrl, supabaseAnonKey, {
|
try {
|
||||||
cookies: {
|
cookiesToSet.forEach(({ name, value, options }) =>
|
||||||
getAll() {
|
cookieStore.set(name, value, options)
|
||||||
return cookieStore.getAll();
|
)
|
||||||
|
} 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