Changes from background agent bc-654437a1-f840-4d59-90ee-87a733d631b1

This commit is contained in:
Cursor Agent 2025-08-11 21:26:34 +00:00
parent 48fa337303
commit 6b68d16461
4 changed files with 102 additions and 59 deletions

View File

@ -1,7 +1,6 @@
'use server';
import type { RequestInit } from 'next/dist/server/web/spec-extension/request';
import { cookies } from 'next/headers';
import { createSupabaseServerClient } from '@/lib/supabase/server';
import { BASE_URL } from './buster_rest/config';
import type { RustApiError } from './buster_rest/errors';
@ -64,8 +63,7 @@ export const serverFetch = async <T>(url: string, config: FetchConfig = {}): Pro
};
export const getSupabaseTokenFromCookies = async () => {
const cookiesManager = await cookies();
const tokenCookie =
cookiesManager.get('sb-127-auth-token') || cookiesManager.get('next-sb-access-token');
return tokenCookie?.value || '';
const supabase = await createSupabaseServerClient();
const sessionData = await supabase.auth.getSession();
return sessionData.data?.session?.access_token || '';
};

View File

@ -27,26 +27,54 @@ const useSupabaseContextInternal = ({
!supabaseContext.user?.id || supabaseContext.user?.is_anonymous === true;
const getExpiresAt = useMemoizedFn((token?: string) => {
try {
const decoded = jwtDecode(token || accessToken);
const expiresAtDecoded = decoded?.exp || 0;
const expiresAtDecoded = (decoded as { exp?: number } | undefined)?.exp || 0;
const ms = millisecondsFromUnixTimestamp(expiresAtDecoded);
return ms;
} catch {
// If token is missing/invalid, report that it is effectively expired now
return 0;
}
});
const onUpdateToken = useMemoizedFn(
async ({ accessToken, expiresAt: _expiresAt }: { accessToken: string; expiresAt: number }) => {
setAccessToken(accessToken);
flushSync(() => {
//noop
});
}
);
const checkTokenValidity = useMemoizedFn(async () => {
try {
const ms = getExpiresAt();
const minutesUntilExpiration = ms / 60000;
const isTokenExpired = minutesUntilExpiration < PREEMTIVE_REFRESH_MINUTES; //5 minutes
// Anonymous users do not require refresh; return current token as-is
if (isAnonymousUser) {
return {
access_token: accessToken,
isTokenValid: isTokenExpired
isTokenValid: true
};
}
if (isTokenExpired) {
// If we don't have a token in memory, try to recover it from Supabase session
if (!accessToken) {
const { data: sessionData } = await supabase.auth.getSession();
const recoveredToken = sessionData.session?.access_token || '';
if (recoveredToken) {
await onUpdateToken({ accessToken: recoveredToken, expiresAt: sessionData.session?.expires_at ?? 0 });
return {
access_token: recoveredToken,
isTokenValid: true
};
}
}
const msUntilExpiration = getExpiresAt();
const minutesUntilExpiration = msUntilExpiration / 60000;
const needsPreemptiveRefresh = minutesUntilExpiration < PREEMTIVE_REFRESH_MINUTES;
if (needsPreemptiveRefresh) {
const { data: refreshedSession, error: refreshedSessionError } =
await supabase.auth.refreshSession();
@ -56,16 +84,23 @@ const useSupabaseContextInternal = ({
description: 'Please refresh the page and try again',
duration: 120 * 1000 //2 minutes
});
throw refreshedSessionError;
// As a fallback, try to read whatever session is available
const { data: sessionData } = await supabase.auth.getSession();
const fallbackToken = sessionData.session?.access_token || '';
if (fallbackToken) {
await onUpdateToken({ accessToken: fallbackToken, expiresAt: sessionData.session?.expires_at ?? 0 });
return { access_token: fallbackToken, isTokenValid: true };
}
throw refreshedSessionError || new Error('Failed to refresh session');
}
const accessToken = refreshedSession.session?.access_token;
const refreshedAccessToken = refreshedSession.session?.access_token;
const expiresAt = refreshedSession.session?.expires_at ?? 0;
await onUpdateToken({ accessToken, expiresAt });
await onUpdateToken({ accessToken: refreshedAccessToken, expiresAt });
await timeout(25);
return {
access_token: accessToken,
access_token: refreshedAccessToken,
isTokenValid: true
};
}
@ -85,15 +120,6 @@ const useSupabaseContextInternal = ({
}
});
const onUpdateToken = useMemoizedFn(
async ({ accessToken, expiresAt: _expiresAt }: { accessToken: string; expiresAt: number }) => {
setAccessToken(accessToken);
flushSync(() => {
//noop
});
}
);
useLayoutEffect(() => {
if (supabaseContext.accessToken) {
setAccessToken(supabaseContext.accessToken);
@ -106,6 +132,10 @@ const useSupabaseContextInternal = ({
const refreshBuffer = PREEMTIVE_REFRESH_MINUTES * 60000; // Refresh minutes before expiration
const timeUntilRefresh = Math.max(0, expiresInMs - refreshBuffer);
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}
// Set up timer for future refresh
refreshTimerRef.current = setTimeout(() => {
checkTokenValidity();
@ -120,7 +150,40 @@ const useSupabaseContextInternal = ({
clearTimeout(refreshTimerRef.current);
}
};
}, [accessToken, checkTokenValidity]);
}, [accessToken, checkTokenValidity, getExpiresAt]);
useEffect(() => {
// Keep access token in sync with Supabase client (captures auto-refresh events)
const { data: subscription } = supabase.auth.onAuthStateChange((_event, session) => {
const newToken = session?.access_token;
if (newToken) {
void onUpdateToken({ accessToken: newToken, expiresAt: session?.expires_at ?? 0 });
}
});
return () => {
subscription.subscription.unsubscribe();
};
}, [onUpdateToken]);
useEffect(() => {
const onVisibility = () => {
if (document.visibilityState === 'visible') {
void checkTokenValidity();
}
};
const onOnline = () => {
void checkTokenValidity();
};
window.addEventListener('visibilitychange', onVisibility);
window.addEventListener('online', onOnline);
return () => {
window.removeEventListener('visibilitychange', onVisibility);
window.removeEventListener('online', onOnline);
};
}, [checkTokenValidity]);
return {
isAnonymousUser,

View File

@ -30,43 +30,25 @@ export async function updateSession(request: NextRequest) {
}
);
// Do not run code between createServerClient and
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
// issues with users being randomly logged out.
// Get the session data first
// const { data: sessionData } = await supabase.auth.getSession();
const { data: sessionData } = await supabase.auth.getSession();
// // Check if session needs refresh (less than 50 minutes until expiry)
// if (sessionData.session?.expires_at) {
// const expiresAtTimestamp = sessionData.session.expires_at * 1000; // Convert to ms
// const now = Date.now();
// const timeUntilExpiry = expiresAtTimestamp - now;
// const fiftyMinutesInMs = 50 * 60 * 1000;
// Preemptively refresh if expiring soon (within 5 minutes)
if (sessionData.session?.expires_at) {
const expiresAtTimestamp = sessionData.session.expires_at * 1000; // ms
const now = Date.now();
const timeUntilExpiry = expiresAtTimestamp - now;
const refreshWindowMs = 5 * 60 * 1000; // 5 minutes
// if (timeUntilExpiry < fiftyMinutesInMs) {
// // Refresh the session
// await supabase.auth.refreshSession();
// }
// }
if (timeUntilExpiry < refreshWindowMs) {
await supabase.auth.refreshSession();
}
}
// Get the user (this will use the refreshed session if we refreshed it)
const {
data: { user }
} = await supabase.auth.getUser();
// 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, user] as [NextResponse, User | null];
}

File diff suppressed because one or more lines are too long