diff --git a/apps/web/src/api/auth_helpers/cookie-helpers.ts b/apps/web/src/api/auth_helpers/cookie-helpers.ts index f77efec97..a8ad3580b 100644 --- a/apps/web/src/api/auth_helpers/cookie-helpers.ts +++ b/apps/web/src/api/auth_helpers/cookie-helpers.ts @@ -1,32 +1,4 @@ -import { parse } from '@supabase/ssr'; -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; - } -}; +let supabaseCookieName = ''; function parseJwt(payload: string) { // Remove the prefix if present @@ -44,19 +16,43 @@ function parseJwt(payload: string) { return JSON.parse(jsonStr); } -export const getSupabaseCookie = () => { - const supabaseCookieRaw = Object.entries(parseCookies()).find( - ([name]) => name.startsWith('sb-') && name.endsWith('-auth-token') - )?.[1]; - if (!supabaseCookieRaw) { - return ''; - } +export const getSupabaseCookieClient = async () => { + const supabaseCookieRaw = await getSupabaseCookieRawClient(); - try { - const decodedCookie = parseJwt(supabaseCookieRaw); - } catch (error) { - console.error('nope'); - } + const decodedCookie = parseJwt(supabaseCookieRaw || '') as { + access_token: string; + expires_at: number; + expires_in: number; + token_type: 'bearer'; + user: { + id: string; + is_anonymous: boolean; + email: string; + }; + }; - return ''; + 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())); +} diff --git a/apps/web/src/api/server-functions/getAppLayout.ts b/apps/web/src/api/server-functions/getAppLayout.ts index 5e37173f3..ce1027f33 100644 --- a/apps/web/src/api/server-functions/getAppLayout.ts +++ b/apps/web/src/api/server-functions/getAppLayout.ts @@ -1,5 +1,3 @@ -import { createServerFn } from '@tanstack/react-start'; -import { getCookie } from '@tanstack/react-start/server'; import Cookies from 'js-cookie'; import type { LayoutSize } from '@/components/ui/layouts/AppLayout'; import { createAutoSaveId } from '@/components/ui/layouts/AppSplitter/create-auto-save-id'; diff --git a/apps/web/src/components/ui/layouts/AppSplitter/AppSplitter.stories.tsx b/apps/web/src/components/ui/layouts/AppSplitter/AppSplitter.stories.tsx index dd44e9419..66bb3acd0 100644 --- a/apps/web/src/components/ui/layouts/AppSplitter/AppSplitter.stories.tsx +++ b/apps/web/src/components/ui/layouts/AppSplitter/AppSplitter.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import cookies from 'js-cookie'; +import Cookies from 'js-cookie'; import React, { useRef } from 'react'; import { Button } from '@/components/ui/buttons/Button'; import { Text } from '@/components/ui/typography/Text'; @@ -83,7 +83,7 @@ export const PassingInitialLayoutFromCookies: Story = { const tagRaw = 'this-is-a-test'; const tag = createAutoSaveId(tagRaw); const initialLayoutFromCookie: { value: number } = JSON.parse( - cookies.get(tag) ?? '["100px", "auto"]' + Cookies.get(tag) ?? '["100px", "auto"]' ); const initialLayout: LayoutSize = [ @@ -589,11 +589,11 @@ const ThreePanelWithAnimationExample = () => { }; const initialLayoutParent = parseLayout( - cookies.get('app-splitter-three-panel-outer') ?? '', + Cookies.get('app-splitter-three-panel-outer') ?? '', 'left' ); const initialLayoutInner = parseLayout( - cookies.get('app-splitter-three-panel-inner') ?? '', + Cookies.get('app-splitter-three-panel-inner') ?? '', 'right' ); diff --git a/apps/web/src/hooks/useCookieState.tsx b/apps/web/src/hooks/useCookieState.tsx index 090d65940..0d81db113 100644 --- a/apps/web/src/hooks/useCookieState.tsx +++ b/apps/web/src/hooks/useCookieState.tsx @@ -1,5 +1,5 @@ import { isServer } from '@tanstack/react-query'; -import cookies from 'js-cookie'; +import Cookies from 'js-cookie'; import { useState } from 'react'; import { useMemoizedFn } from './useMemoizedFn'; import { useMount } from './useMount'; @@ -43,7 +43,7 @@ const setCookie = ( const expires = new Date(Date.now() + expirationTime); const { domain, path = '/', secure = true, sameSite = 'lax' } = options; - cookies.set(name, value, { + Cookies.set(name, value, { expires, path, domain, @@ -57,7 +57,7 @@ const removeCookie = (name: string, options: CookieOptions = {}): void => { if (isServer) return; const { domain, path = '/' } = options; - cookies.remove(name, { path, domain }); + Cookies.remove(name, { path, domain }); }; export function useCookieState( @@ -79,7 +79,7 @@ export function useCookieState( 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 => { // Prefer explicitly provided initialValue if present if (initialValue !== undefined) { @@ -87,7 +87,7 @@ export function useCookieState( } try { - const cookieValue = cookies.get(key); + const cookieValue = Cookies.get(key); if (!cookieValue) { return executeBustStorage(); @@ -128,7 +128,7 @@ export function useCookieState( const [state, setState] = useState(() => getInitialValue()); - // Initialize state from cookies on mount + // Initialize state from Cookies on mount useMount(() => { setState(getInitialValue()); }); diff --git a/apps/web/src/integrations/supabase/getSupabaseUserClient.ts b/apps/web/src/integrations/supabase/getSupabaseUserClient.ts index 7b433c601..081cc0729 100644 --- a/apps/web/src/integrations/supabase/getSupabaseUserClient.ts +++ b/apps/web/src/integrations/supabase/getSupabaseUserClient.ts @@ -1,9 +1,10 @@ -import { isTokenExpired } from '@/api/auth_helpers/expiration-helpers'; +import { isTokenAlmostExpired, isTokenExpired } from '@/api/auth_helpers/expiration-helpers'; import { getSupabaseSessionServerFn, getSupabaseUserServerFn, } from '@/api/server-functions/getSupabaseSession'; import { isServer } from '@/lib/window'; +import { getSupabaseCookieClient } from '../../api/auth_helpers/cookie-helpers'; import { getBrowserClient } from './client'; const supabase = getBrowserClient(); @@ -11,7 +12,7 @@ const supabase = getBrowserClient(); export const getSupabaseSession = async () => { const { data: sessionData, error: sessionError } = isServer ? 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) { 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 () => { const { data: userData } = isServer ? await getSupabaseUserServerFn() diff --git a/apps/web/src/lib/timezone.ts b/apps/web/src/lib/timezone.ts index 6b17313a4..ef479fc19 100644 --- a/apps/web/src/lib/timezone.ts +++ b/apps/web/src/lib/timezone.ts @@ -1,7 +1,7 @@ import dayjs from 'dayjs'; import timezone from 'dayjs/plugin/timezone'; import utc from 'dayjs/plugin/utc'; -import cookies from 'js-cookie'; +import Cookies from 'js-cookie'; import { getServerCookie } from '@/api/server-functions/getServerCookie'; import { isServer } from './window'; @@ -30,7 +30,7 @@ export const detectClientTimezone = (): string => { export const setTimezoneInCookie = (timezone: string): void => { if (isServer || !timezone) return; - cookies.set(TIMEZONE_COOKIE_KEY, timezone, { + Cookies.set(TIMEZONE_COOKIE_KEY, timezone, { expires: TIMEZONE_COOKIE_EXPIRY, sameSite: 'lax', secure: true, @@ -46,7 +46,7 @@ export const getTimezoneFromCookie = async (): Promise => { return cookieData || null; } - return cookies.get(TIMEZONE_COOKIE_KEY) || null; + return Cookies.get(TIMEZONE_COOKIE_KEY) || null; }; /**