supabase session with a cookie

This commit is contained in:
Nate Kelley 2025-09-15 19:47:55 -06:00
parent 9fd2355e74
commit 02a295d90a
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
6 changed files with 77 additions and 59 deletions

View File

@ -1,32 +1,4 @@
import { parse } from '@supabase/ssr'; let supabaseCookieName = '';
import { parseCookies } from '@tanstack/react-start/server';
import { jwtDecode } from 'jwt-decode';
import { getExpiresAtMilliseconds } from './expiration-helpers';
export const checkTokenValidityFromToken = (token: string | undefined) => {
const decoded = decodeJwtToken(token);
};
export const decodeJwtToken = (token: string | undefined) => {
try {
const decoded = jwtDecode(token || '');
return decoded as { exp?: number };
} catch {
console.error('Error decoding token', token);
return null;
}
};
export const getExpiresAtFromToken = (token: string | undefined) => {
try {
const expiresAtDecoded = decodeJwtToken(token)?.exp ?? 0;
return getExpiresAtMilliseconds(expiresAtDecoded);
} catch {
console.error('Error decoding token', token);
// If token is missing/invalid, report that it is effectively expired now
return 0;
}
};
function parseJwt(payload: string) { function parseJwt(payload: string) {
// Remove the prefix if present // Remove the prefix if present
@ -44,19 +16,43 @@ function parseJwt(payload: string) {
return JSON.parse(jsonStr); return JSON.parse(jsonStr);
} }
export const getSupabaseCookie = () => { export const getSupabaseCookieClient = async () => {
const supabaseCookieRaw = Object.entries(parseCookies()).find( const supabaseCookieRaw = await getSupabaseCookieRawClient();
([name]) => name.startsWith('sb-') && name.endsWith('-auth-token')
)?.[1];
if (!supabaseCookieRaw) {
return '';
}
try { const decodedCookie = parseJwt(supabaseCookieRaw || '') as {
const decodedCookie = parseJwt(supabaseCookieRaw); access_token: string;
} catch (error) { expires_at: number;
console.error('nope'); expires_in: number;
} token_type: 'bearer';
user: {
return ''; id: string;
is_anonymous: boolean;
email: string;
}; };
};
return decodedCookie;
};
function listAllCookies() {
return Object.fromEntries(
document.cookie.split('; ').map((cookieStr) => {
const [name, ...rest] = cookieStr.split('=');
return [name, rest.join('=')];
})
);
}
async function getSupabaseCookieRawClient() {
const getCookieByKey = (d: [string, string][]) => {
const cookie = d.find(([name]) => name.startsWith('sb-') && name.endsWith('-auth-token'));
supabaseCookieName = cookie?.[0] || '';
return cookie?.[1];
};
if (supabaseCookieName) {
const Cookies = await import('js-cookie').then((m) => m.default);
return Cookies.get(supabaseCookieName);
}
return getCookieByKey(Object.entries(listAllCookies()));
}

View File

@ -1,5 +1,3 @@
import { createServerFn } from '@tanstack/react-start';
import { getCookie } from '@tanstack/react-start/server';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import type { LayoutSize } from '@/components/ui/layouts/AppLayout'; import type { LayoutSize } from '@/components/ui/layouts/AppLayout';
import { createAutoSaveId } from '@/components/ui/layouts/AppSplitter/create-auto-save-id'; import { createAutoSaveId } from '@/components/ui/layouts/AppSplitter/create-auto-save-id';

View File

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-vite'; import type { Meta, StoryObj } from '@storybook/react-vite';
import cookies from 'js-cookie'; import Cookies from 'js-cookie';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { Button } from '@/components/ui/buttons/Button'; import { Button } from '@/components/ui/buttons/Button';
import { Text } from '@/components/ui/typography/Text'; import { Text } from '@/components/ui/typography/Text';
@ -83,7 +83,7 @@ export const PassingInitialLayoutFromCookies: Story = {
const tagRaw = 'this-is-a-test'; const tagRaw = 'this-is-a-test';
const tag = createAutoSaveId(tagRaw); const tag = createAutoSaveId(tagRaw);
const initialLayoutFromCookie: { value: number } = JSON.parse( const initialLayoutFromCookie: { value: number } = JSON.parse(
cookies.get(tag) ?? '["100px", "auto"]' Cookies.get(tag) ?? '["100px", "auto"]'
); );
const initialLayout: LayoutSize = [ const initialLayout: LayoutSize = [
@ -589,11 +589,11 @@ const ThreePanelWithAnimationExample = () => {
}; };
const initialLayoutParent = parseLayout( const initialLayoutParent = parseLayout(
cookies.get('app-splitter-three-panel-outer') ?? '', Cookies.get('app-splitter-three-panel-outer') ?? '',
'left' 'left'
); );
const initialLayoutInner = parseLayout( const initialLayoutInner = parseLayout(
cookies.get('app-splitter-three-panel-inner') ?? '', Cookies.get('app-splitter-three-panel-inner') ?? '',
'right' 'right'
); );

View File

@ -1,5 +1,5 @@
import { isServer } from '@tanstack/react-query'; import { isServer } from '@tanstack/react-query';
import cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { useState } from 'react'; import { useState } from 'react';
import { useMemoizedFn } from './useMemoizedFn'; import { useMemoizedFn } from './useMemoizedFn';
import { useMount } from './useMount'; import { useMount } from './useMount';
@ -43,7 +43,7 @@ const setCookie = (
const expires = new Date(Date.now() + expirationTime); const expires = new Date(Date.now() + expirationTime);
const { domain, path = '/', secure = true, sameSite = 'lax' } = options; const { domain, path = '/', secure = true, sameSite = 'lax' } = options;
cookies.set(name, value, { Cookies.set(name, value, {
expires, expires,
path, path,
domain, domain,
@ -57,7 +57,7 @@ const removeCookie = (name: string, options: CookieOptions = {}): void => {
if (isServer) return; if (isServer) return;
const { domain, path = '/' } = options; const { domain, path = '/' } = options;
cookies.remove(name, { path, domain }); Cookies.remove(name, { path, domain });
}; };
export function useCookieState<T>( export function useCookieState<T>(
@ -79,7 +79,7 @@ export function useCookieState<T>(
return typeof defaultValue === 'function' ? (defaultValue as () => T)() : defaultValue; return typeof defaultValue === 'function' ? (defaultValue as () => T)() : defaultValue;
}); });
// Get initial value from cookies or use default // Get initial value from Cookies or use default
const getInitialValue = useMemoizedFn((): T | undefined => { const getInitialValue = useMemoizedFn((): T | undefined => {
// Prefer explicitly provided initialValue if present // Prefer explicitly provided initialValue if present
if (initialValue !== undefined) { if (initialValue !== undefined) {
@ -87,7 +87,7 @@ export function useCookieState<T>(
} }
try { try {
const cookieValue = cookies.get(key); const cookieValue = Cookies.get(key);
if (!cookieValue) { if (!cookieValue) {
return executeBustStorage(); return executeBustStorage();
@ -128,7 +128,7 @@ export function useCookieState<T>(
const [state, setState] = useState<T | undefined>(() => getInitialValue()); const [state, setState] = useState<T | undefined>(() => getInitialValue());
// Initialize state from cookies on mount // Initialize state from Cookies on mount
useMount(() => { useMount(() => {
setState(getInitialValue()); setState(getInitialValue());
}); });

View File

@ -1,9 +1,10 @@
import { isTokenExpired } from '@/api/auth_helpers/expiration-helpers'; import { isTokenAlmostExpired, isTokenExpired } from '@/api/auth_helpers/expiration-helpers';
import { import {
getSupabaseSessionServerFn, getSupabaseSessionServerFn,
getSupabaseUserServerFn, getSupabaseUserServerFn,
} from '@/api/server-functions/getSupabaseSession'; } from '@/api/server-functions/getSupabaseSession';
import { isServer } from '@/lib/window'; import { isServer } from '@/lib/window';
import { getSupabaseCookieClient } from '../../api/auth_helpers/cookie-helpers';
import { getBrowserClient } from './client'; import { getBrowserClient } from './client';
const supabase = getBrowserClient(); const supabase = getBrowserClient();
@ -11,7 +12,7 @@ const supabase = getBrowserClient();
export const getSupabaseSession = async () => { export const getSupabaseSession = async () => {
const { data: sessionData, error: sessionError } = isServer const { data: sessionData, error: sessionError } = isServer
? await getSupabaseSessionServerFn() ? await getSupabaseSessionServerFn()
: await supabase.auth.getSession(); //10 - 15ms locally, maybe consider getting it from the cookie instead. console the supabase object it had it there. : await getClientSupabaseSessionFast();
if ((!sessionData?.session || sessionError) && !isServer) { if ((!sessionData?.session || sessionError) && !isServer) {
return { return {
@ -30,6 +31,29 @@ export const getSupabaseSession = async () => {
}; };
}; };
const getClientSupabaseSessionFast = async () => {
try {
const cookieRes = await getSupabaseCookieClient();
const almostExpired = isTokenAlmostExpired(cookieRes.expires_at);
if (!almostExpired) {
return {
data: {
session: {
access_token: cookieRes.access_token,
isExpired: false,
expires_at: cookieRes.expires_at,
},
},
error: null,
};
}
} catch (error) {
//fail silently
}
return await supabase.auth.getSession(); //100ms on server, that's why we're using the cookie instead.
};
export const getSupabaseUser = async () => { export const getSupabaseUser = async () => {
const { data: userData } = isServer const { data: userData } = isServer
? await getSupabaseUserServerFn() ? await getSupabaseUserServerFn()

View File

@ -1,7 +1,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone'; import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
import cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { getServerCookie } from '@/api/server-functions/getServerCookie'; import { getServerCookie } from '@/api/server-functions/getServerCookie';
import { isServer } from './window'; import { isServer } from './window';
@ -30,7 +30,7 @@ export const detectClientTimezone = (): string => {
export const setTimezoneInCookie = (timezone: string): void => { export const setTimezoneInCookie = (timezone: string): void => {
if (isServer || !timezone) return; if (isServer || !timezone) return;
cookies.set(TIMEZONE_COOKIE_KEY, timezone, { Cookies.set(TIMEZONE_COOKIE_KEY, timezone, {
expires: TIMEZONE_COOKIE_EXPIRY, expires: TIMEZONE_COOKIE_EXPIRY,
sameSite: 'lax', sameSite: 'lax',
secure: true, secure: true,
@ -46,7 +46,7 @@ export const getTimezoneFromCookie = async (): Promise<string | null> => {
return cookieData || null; return cookieData || null;
} }
return cookies.get(TIMEZONE_COOKIE_KEY) || null; return Cookies.get(TIMEZONE_COOKIE_KEY) || null;
}; };
/** /**