mirror of https://github.com/buster-so/buster.git
Changes from background agent bc-654437a1-f840-4d59-90ee-87a733d631b1
This commit is contained in:
parent
48fa337303
commit
6b68d16461
|
@ -1,7 +1,6 @@
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import type { RequestInit } from 'next/dist/server/web/spec-extension/request';
|
import type { RequestInit } from 'next/dist/server/web/spec-extension/request';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { createSupabaseServerClient } from '@/lib/supabase/server';
|
import { createSupabaseServerClient } from '@/lib/supabase/server';
|
||||||
import { BASE_URL } from './buster_rest/config';
|
import { BASE_URL } from './buster_rest/config';
|
||||||
import type { RustApiError } from './buster_rest/errors';
|
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 () => {
|
export const getSupabaseTokenFromCookies = async () => {
|
||||||
const cookiesManager = await cookies();
|
const supabase = await createSupabaseServerClient();
|
||||||
const tokenCookie =
|
const sessionData = await supabase.auth.getSession();
|
||||||
cookiesManager.get('sb-127-auth-token') || cookiesManager.get('next-sb-access-token');
|
return sessionData.data?.session?.access_token || '';
|
||||||
return tokenCookie?.value || '';
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,26 +27,54 @@ const useSupabaseContextInternal = ({
|
||||||
!supabaseContext.user?.id || supabaseContext.user?.is_anonymous === true;
|
!supabaseContext.user?.id || supabaseContext.user?.is_anonymous === true;
|
||||||
|
|
||||||
const getExpiresAt = useMemoizedFn((token?: string) => {
|
const getExpiresAt = useMemoizedFn((token?: string) => {
|
||||||
const decoded = jwtDecode(token || accessToken);
|
try {
|
||||||
const expiresAtDecoded = decoded?.exp || 0;
|
const decoded = jwtDecode(token || accessToken);
|
||||||
const ms = millisecondsFromUnixTimestamp(expiresAtDecoded);
|
const expiresAtDecoded = (decoded as { exp?: number } | undefined)?.exp || 0;
|
||||||
return ms;
|
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 () => {
|
const checkTokenValidity = useMemoizedFn(async () => {
|
||||||
try {
|
try {
|
||||||
const ms = getExpiresAt();
|
// Anonymous users do not require refresh; return current token as-is
|
||||||
const minutesUntilExpiration = ms / 60000;
|
|
||||||
const isTokenExpired = minutesUntilExpiration < PREEMTIVE_REFRESH_MINUTES; //5 minutes
|
|
||||||
|
|
||||||
if (isAnonymousUser) {
|
if (isAnonymousUser) {
|
||||||
return {
|
return {
|
||||||
access_token: accessToken,
|
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 } =
|
const { data: refreshedSession, error: refreshedSessionError } =
|
||||||
await supabase.auth.refreshSession();
|
await supabase.auth.refreshSession();
|
||||||
|
|
||||||
|
@ -56,16 +84,23 @@ const useSupabaseContextInternal = ({
|
||||||
description: 'Please refresh the page and try again',
|
description: 'Please refresh the page and try again',
|
||||||
duration: 120 * 1000 //2 minutes
|
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;
|
const expiresAt = refreshedSession.session?.expires_at ?? 0;
|
||||||
|
|
||||||
await onUpdateToken({ accessToken, expiresAt });
|
await onUpdateToken({ accessToken: refreshedAccessToken, expiresAt });
|
||||||
await timeout(25);
|
await timeout(25);
|
||||||
return {
|
return {
|
||||||
access_token: accessToken,
|
access_token: refreshedAccessToken,
|
||||||
isTokenValid: true
|
isTokenValid: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -85,15 +120,6 @@ const useSupabaseContextInternal = ({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const onUpdateToken = useMemoizedFn(
|
|
||||||
async ({ accessToken, expiresAt: _expiresAt }: { accessToken: string; expiresAt: number }) => {
|
|
||||||
setAccessToken(accessToken);
|
|
||||||
flushSync(() => {
|
|
||||||
//noop
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (supabaseContext.accessToken) {
|
if (supabaseContext.accessToken) {
|
||||||
setAccessToken(supabaseContext.accessToken);
|
setAccessToken(supabaseContext.accessToken);
|
||||||
|
@ -106,6 +132,10 @@ const useSupabaseContextInternal = ({
|
||||||
const refreshBuffer = PREEMTIVE_REFRESH_MINUTES * 60000; // Refresh minutes before expiration
|
const refreshBuffer = PREEMTIVE_REFRESH_MINUTES * 60000; // Refresh minutes before expiration
|
||||||
const timeUntilRefresh = Math.max(0, expiresInMs - refreshBuffer);
|
const timeUntilRefresh = Math.max(0, expiresInMs - refreshBuffer);
|
||||||
|
|
||||||
|
if (refreshTimerRef.current) {
|
||||||
|
clearTimeout(refreshTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
// Set up timer for future refresh
|
// Set up timer for future refresh
|
||||||
refreshTimerRef.current = setTimeout(() => {
|
refreshTimerRef.current = setTimeout(() => {
|
||||||
checkTokenValidity();
|
checkTokenValidity();
|
||||||
|
@ -120,7 +150,40 @@ const useSupabaseContextInternal = ({
|
||||||
clearTimeout(refreshTimerRef.current);
|
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 {
|
return {
|
||||||
isAnonymousUser,
|
isAnonymousUser,
|
||||||
|
|
|
@ -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
|
// 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)
|
// Preemptively refresh if expiring soon (within 5 minutes)
|
||||||
// if (sessionData.session?.expires_at) {
|
if (sessionData.session?.expires_at) {
|
||||||
// const expiresAtTimestamp = sessionData.session.expires_at * 1000; // Convert to ms
|
const expiresAtTimestamp = sessionData.session.expires_at * 1000; // ms
|
||||||
// const now = Date.now();
|
const now = Date.now();
|
||||||
// const timeUntilExpiry = expiresAtTimestamp - now;
|
const timeUntilExpiry = expiresAtTimestamp - now;
|
||||||
// const fiftyMinutesInMs = 50 * 60 * 1000;
|
const refreshWindowMs = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
// if (timeUntilExpiry < fiftyMinutesInMs) {
|
if (timeUntilExpiry < refreshWindowMs) {
|
||||||
// // Refresh the session
|
await supabase.auth.refreshSession();
|
||||||
// await supabase.auth.refreshSession();
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// Get the user (this will use the refreshed session if we refreshed it)
|
// Get the user (this will use the refreshed session if we refreshed it)
|
||||||
const {
|
const {
|
||||||
data: { user }
|
data: { user }
|
||||||
} = await supabase.auth.getUser();
|
} = 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];
|
return [supabaseResponse, user] as [NextResponse, User | null];
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue