unique ids

This commit is contained in:
Nate Kelley 2025-08-15 13:16:54 -06:00
parent 8b22aafa2c
commit fb618736e3
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
6 changed files with 30 additions and 158 deletions

View File

@ -1,144 +0,0 @@
import axios, { type AxiosError, AxiosHeaders, type InternalAxiosRequestConfig } from 'axios';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createAxiosInstance, defaultAxiosRequestHandler } from './createAxiosInstance';
import { rustErrorHandler } from './errors';
// Mock dependencies
vi.mock('axios');
vi.mock('./buster_rest/errors');
vi.mock('./createServerInstance');
vi.mock('@tanstack/react-query', () => ({
isServer: false,
}));
describe('createAxiosInstance', () => {
const mockBaseURL = 'https://api.example.com';
beforeEach(() => {
vi.clearAllMocks();
(axios.create as any).mockReturnValue({
interceptors: {
response: { use: vi.fn() },
request: { use: vi.fn() },
},
});
});
it('creates an axios instance with correct configuration', () => {
createAxiosInstance(mockBaseURL);
expect(axios.create).toHaveBeenCalledWith({
baseURL: mockBaseURL,
timeout: 120000,
headers: {
'Content-Type': 'application/json',
},
});
});
it('sets up response interceptors', () => {
const mockInstance = {
interceptors: {
response: { use: vi.fn() },
request: { use: vi.fn() },
},
};
(axios.create as any).mockReturnValue(mockInstance);
createAxiosInstance(mockBaseURL);
expect(mockInstance.interceptors.response.use).toHaveBeenCalled();
expect(mockInstance.interceptors.request.use).toHaveBeenCalledWith(defaultAxiosRequestHandler);
});
it('handles errors in response interceptor', async () => {
const mockError = new Error('API Error') as AxiosError;
const mockInstance = {
interceptors: {
response: { use: vi.fn() },
request: { use: vi.fn() },
},
};
(axios.create as any).mockReturnValue(mockInstance);
(rustErrorHandler as any).mockReturnValue('Processed Error');
// Get the error handler by capturing the second argument passed to use()
createAxiosInstance(mockBaseURL);
const errorHandler = mockInstance.interceptors.response.use.mock.calls[0][1];
// Test the error handler
await expect(errorHandler(mockError)).rejects.toBe('Processed Error');
expect(rustErrorHandler).toHaveBeenCalledWith(mockError);
});
});
describe('defaultAxiosRequestHandler', () => {
const mockConfig: InternalAxiosRequestConfig = {
headers: new AxiosHeaders(),
method: 'get',
url: 'test',
};
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
it('adds authorization header with token in client environment', async () => {
const mockToken = 'test-token';
const mockCheckTokenValidity = vi.fn().mockResolvedValue({
access_token: mockToken,
isTokenValid: true,
});
const result = await defaultAxiosRequestHandler(mockConfig, {
checkTokenValidity: () => Promise.resolve(mockCheckTokenValidity()),
});
expect(result.headers.Authorization).toBe(`Bearer ${mockToken}`);
});
it('throws error when token is empty', async () => {
const mockCheckTokenValidity = vi.fn().mockResolvedValue({
access_token: '',
isTokenValid: false,
});
// The function should throw an error when no token is available
await expect(
defaultAxiosRequestHandler(mockConfig, {
checkTokenValidity: () => Promise.resolve(mockCheckTokenValidity()),
})
).rejects.toThrow('User authentication error - failed to get valid token');
});
it('throws error when checkTokenValidity fails', async () => {
const mockCheckTokenValidity = vi.fn().mockRejectedValue(new Error('Token validation failed'));
// The function should throw an error when token validation fails
await expect(
defaultAxiosRequestHandler(mockConfig, {
checkTokenValidity: () => Promise.reject(mockCheckTokenValidity()),
})
).rejects.toThrow('User authentication error - failed to get valid token');
});
it('preserves existing config properties', async () => {
const originalConfig: InternalAxiosRequestConfig = {
...mockConfig,
timeout: 5000,
baseURL: 'https://api.example.com',
};
const result = await defaultAxiosRequestHandler(originalConfig, {
checkTokenValidity: () =>
Promise.resolve({
access_token: 'token',
isTokenValid: true,
}),
});
expect(result.timeout).toBe(5000);
expect(result.baseURL).toBe('https://api.example.com');
});
});

View File

@ -1,4 +1,4 @@
import type React from 'react'; import React from 'react';
export const SupabaseIcon: React.FC<{ export const SupabaseIcon: React.FC<{
onClick?: () => void; onClick?: () => void;
@ -6,6 +6,9 @@ export const SupabaseIcon: React.FC<{
size?: number; size?: number;
className?: string; className?: string;
}> = (props) => { }> = (props) => {
const uniqueId = React.useId();
const gradientId = `paint0_linear-${uniqueId}`;
return ( return (
<svg <svg
{...props} {...props}
@ -18,11 +21,11 @@ export const SupabaseIcon: React.FC<{
<title>Supabase</title> <title>Supabase</title>
<path <path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
fill="url(#paint0_linear)" fill={`url(#${gradientId})`}
/> />
<path <path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
fill="url(#paint1_linear)" fill={`url(#overlay-${uniqueId})`}
fillOpacity="0.2" fillOpacity="0.2"
/> />
<path <path
@ -31,7 +34,7 @@ export const SupabaseIcon: React.FC<{
/> />
<defs> <defs>
<linearGradient <linearGradient
id="paint0_linear" id={gradientId}
x1="53.9738" x1="53.9738"
y1="54.974" y1="54.974"
x2="94.1635" x2="94.1635"
@ -42,7 +45,7 @@ export const SupabaseIcon: React.FC<{
<stop offset="1" stopColor="#3ECF8E" /> <stop offset="1" stopColor="#3ECF8E" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="paint1_linear" id={`overlay-${uniqueId}`}
x1="36.1558" x1="36.1558"
y1="30.578" y1="30.578"
x2="54.4844" x2="54.4844"

View File

@ -24,9 +24,10 @@ import {
export const RootProviders: React.FC<PropsWithChildren<SupabaseContextType>> = ({ export const RootProviders: React.FC<PropsWithChildren<SupabaseContextType>> = ({
children, children,
user, user,
accessToken,
}) => { }) => {
return ( return (
<SupabaseContextProvider user={user}> <SupabaseContextProvider user={user} accessToken={accessToken}>
<BusterStyleProvider>{children}</BusterStyleProvider> <BusterStyleProvider>{children}</BusterStyleProvider>
{/* <BusterReactQueryProvider> {/* <BusterReactQueryProvider>
<HydrationBoundary state={dehydrate(queryClient)}> <HydrationBoundary state={dehydrate(queryClient)}>

View File

@ -9,14 +9,19 @@ export type SupabaseContextType = {
id: string; id: string;
is_anonymous: boolean; is_anonymous: boolean;
}; };
accessToken: string;
}; };
const supabase = getBrowserClient(); const supabase = getBrowserClient();
const fiveMinutes = 5 * 60 * 1000; const fiveMinutes = 5 * 60 * 1000;
const useSupabaseContextInternal = ({ user }: SupabaseContextType) => { const useSupabaseContextInternal = ({
user,
accessToken: accessTokenProp,
}: SupabaseContextType) => {
const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const [supabaseUser, setSupabaseUser] = useState<Pick<User, 'id' | 'is_anonymous'> | null>(user); const [supabaseUser, setSupabaseUser] = useState<Pick<User, 'id' | 'is_anonymous'> | null>(user);
const [accessToken, setAccessToken] = useState(accessTokenProp);
const isAnonymousUser: boolean = !user?.id || user?.is_anonymous === true; const isAnonymousUser: boolean = !user?.id || user?.is_anonymous === true;
@ -27,9 +32,12 @@ const useSupabaseContextInternal = ({ user }: SupabaseContextType) => {
const user = session?.user ?? null; const user = session?.user ?? null;
const expiresAt = session?.expires_at ?? 0; const expiresAt = session?.expires_at ?? 0;
const timerMs = expiresAt - fiveMinutes; const timerMs = expiresAt - fiveMinutes;
const accessToken = session?.access_token ?? '';
setSupabaseUser(user); setSupabaseUser(user);
if (accessToken) setAccessToken(accessToken);
if (refreshTimerRef.current) { if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current); clearTimeout(refreshTimerRef.current);
} }
@ -48,7 +56,9 @@ const useSupabaseContextInternal = ({ user }: SupabaseContextType) => {
}); });
return { return {
supabaseUser,
isAnonymousUser, isAnonymousUser,
accessToken,
}; };
}; };
@ -58,8 +68,8 @@ const SupabaseContext = createContext<ReturnType<typeof useSupabaseContextIntern
export const SupabaseContextProvider: React.FC< export const SupabaseContextProvider: React.FC<
SupabaseContextType & { children: React.ReactNode } SupabaseContextType & { children: React.ReactNode }
> = React.memo(({ user, children }) => { > = React.memo(({ user, accessToken, children }) => {
const value = useSupabaseContextInternal({ user }); const value = useSupabaseContextInternal({ user, accessToken });
return <SupabaseContext.Provider value={value}>{children}</SupabaseContext.Provider>; return <SupabaseContext.Provider value={value}>{children}</SupabaseContext.Provider>;
}); });

View File

@ -23,7 +23,7 @@ function transformToAuthUserDTO(user: User): AuthUserDTO {
} }
export const getSupabaseUser = createServerFn({ method: 'GET' }).handler(async () => { export const getSupabaseUser = createServerFn({ method: 'GET' }).handler(async () => {
const supabase = await getSupabaseServerClient(); const supabase = getSupabaseServerClient();
const { data: userData } = await supabase.auth.getUser(); const { data: userData } = await supabase.auth.getUser();
if (!userData.user) { if (!userData.user) {
@ -43,7 +43,7 @@ export const getSupabaseUser = createServerFn({ method: 'GET' }).handler(async (
created_at: anon.data.user?.created_at ?? '', created_at: anon.data.user?.created_at ?? '',
} satisfies AuthUserDTO, } satisfies AuthUserDTO,
accessToken: anon.data.accessToken, accessToken: anon.data.accessToken,
} as { user: AuthUserDTO; accessToken: string | undefined }; } as { user: AuthUserDTO; accessToken: string };
} }
// Get the session first // Get the session first
@ -56,5 +56,5 @@ export const getSupabaseUser = createServerFn({ method: 'GET' }).handler(async (
return { return {
user, user,
accessToken, accessToken,
} as { user: AuthUserDTO; accessToken: string | undefined }; } as { user: AuthUserDTO; accessToken: string };
}); });

View File

@ -33,7 +33,7 @@ export const Route = createRootRouteWithContext<AppRouterContext>()({
}); });
function RootDocument({ children }: { children: React.ReactNode }) { function RootDocument({ children }: { children: React.ReactNode }) {
const { user } = Route.useRouteContext(); const { user, accessToken } = Route.useRouteContext();
return ( return (
<html lang="en"> <html lang="en">
@ -41,7 +41,9 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<HeadContent /> <HeadContent />
</head> </head>
<body> <body>
<RootProviders user={user}>{children}</RootProviders> <RootProviders user={user} accessToken={accessToken}>
{children}
</RootProviders>
<TanstackDevtools /> <TanstackDevtools />
<Scripts /> <Scripts />
</body> </body>