add browser login path

This commit is contained in:
Nate Kelley 2025-10-02 16:27:20 -06:00
parent 065ec68ab0
commit f47891003e
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
6 changed files with 160 additions and 79 deletions

View File

@ -40,6 +40,14 @@ export const prefetchGetMyUserInfo = async (queryClient: QueryClient) => {
return queryClient.getQueryData(userQueryKeys.userGetUserMyself.queryKey);
};
export const ensureGetMyUserInfo = async (queryClient: QueryClient) => {
await queryClient.ensureQueryData({
...userQueryKeys.userGetUserMyself,
queryFn: () => getMyUserInfo(),
});
return queryClient.getQueryData(userQueryKeys.userGetUserMyself.queryKey);
};
export const useGetUser = (params: Parameters<typeof getUser>[0]) => {
const queryFn = () => getUser(params);
return useQuery({

View File

@ -0,0 +1,82 @@
import { chromium } from 'playwright';
import { env } from '@/env';
import { getSupabaseServerClient } from '@/integrations/supabase/server';
export const browserLogin = async ({
accessToken,
width,
height,
fullPath,
request,
}: {
accessToken: string;
width: number;
height: number;
fullPath: string;
request: Request;
}) => {
const supabase = getSupabaseServerClient();
const jwtPayload = JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString());
const origin = new URL(request.url).origin;
const {
data: { user },
} = await supabase.auth.getUser(accessToken);
if (!user || user?.is_anonymous) {
throw new Error('User not found');
}
const session = {
access_token: accessToken,
token_type: 'bearer',
expires_in: 3600,
expires_at: jwtPayload.exp,
refresh_token: '',
user: user,
};
const browser = await chromium.launch();
try {
const context = await browser.newContext({
viewport: { width, height },
});
const cookieKey = (supabase as unknown as { storageKey: string }).storageKey;
// Format cookie value as Supabase expects: base64-<encoded_session>
const cookieValue = `base64-${Buffer.from(JSON.stringify(session)).toString('base64')}`;
await context.addCookies([
{
name: cookieKey,
value: cookieValue,
domain: new URL(env.VITE_PUBLIC_URL).hostname,
path: '/',
httpOnly: true,
secure: true,
sameSite: 'Lax',
},
]);
const page = await context.newPage();
const fullPathWithOrigin = `${origin}${fullPath}`;
page.on('console', (msg) => {
const hasError = msg.type() === 'error';
if (hasError) {
browser.close();
throw new Error(`Error in browser: ${msg.text()}`);
}
});
await page.goto(fullPathWithOrigin, { waitUntil: 'networkidle' });
return { context, browser, page };
} catch (error) {
console.error('Error logging in to browser', error);
await browser.close();
throw error;
}
};

View File

@ -22,3 +22,45 @@ export function defineLinkFromFactory<TFrom extends string>(fromOptions: { from:
export const createFullURL = (location: ParsedLocation | string): string =>
window.location.origin +
(typeof location === 'string' ? (location as string) : (location as ParsedLocation).href);
export const createHrefFromLink = <
TRouter extends RegisteredRouter,
TOptions,
TFrom extends string = string,
>(
link: ILinkProps<TRouter, TOptions, TFrom>
) => {
const buildLink = defineLink(link);
// Start with the 'to' path
let href = typeof buildLink.to === 'string' ? buildLink.to : '';
// Replace path params
if (buildLink.params && typeof buildLink.params === 'object') {
for (const [key, value] of Object.entries(buildLink.params)) {
href = href.replace(`:${key}`, String(value));
href = href.replace(`$${key}`, String(value));
}
}
// Append search params
if (buildLink.search && typeof buildLink.search === 'object') {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(buildLink.search)) {
if (value !== undefined && value !== null) {
searchParams.append(key, String(value));
}
}
const searchString = searchParams.toString();
if (searchString) {
href += `?${searchString}`;
}
}
// Append hash if present
if (buildLink.hash) {
href += buildLink.hash;
}
return href;
};

View File

@ -1,7 +1,5 @@
import { createFileRoute, Outlet } from '@tanstack/react-router';
import { getWebRequest } from '@tanstack/react-start/server';
import { prefetchGetMyUserInfo } from '@/api/buster_rest/users';
import { env } from '@/env';
import { ensureGetMyUserInfo } from '@/api/buster_rest/users';
import { getSupabaseSession } from '@/integrations/supabase/getSupabaseUserClient';
export const Route = createFileRoute('/screenshots/_content')({
@ -9,16 +7,8 @@ export const Route = createFileRoute('/screenshots/_content')({
component: RouteComponent,
beforeLoad: async ({ context }) => {
const user = await getSupabaseSession();
await prefetchGetMyUserInfo(context.queryClient);
return {
user,
};
},
loader: async ({ context }) => {
const { user } = context;
return {
user,
};
await ensureGetMyUserInfo(context.queryClient);
return { user };
},
});

View File

@ -1,6 +1,5 @@
import { createFileRoute } from '@tanstack/react-router';
import { getCookie } from '@tanstack/react-start/server';
import { z } from 'zod';
import { prefetchGetMetric } from '@/api/buster_rest/metrics';
import { useGetUserBasicInfo } from '@/api/buster_rest/users/useGetUserInfo';
import { Route as ScreenshotsRoute } from '../_content';
import { GetMetricScreenshotQuerySchema } from '../metrics.$metricId.index';
@ -9,27 +8,29 @@ export const Route = createFileRoute('/screenshots/_content/metrics/$metricId/co
component: RouteComponent,
validateSearch: GetMetricScreenshotQuerySchema,
ssr: true,
beforeLoad: async ({ context }) => {
const supabaseCookie = await getCookie('sb-127-auth-token');
console.log('--------------------------------');
console.log(supabaseCookie);
console.log('--------------------------------');
beforeLoad: async ({ context, params, search, matches }) => {
const lastMatch = matches[matches.length - 1];
const res = await prefetchGetMetric(context.queryClient, {
id: params.metricId,
version_number: search.version_number,
});
if (!res || true) {
throw new Error('Metric not found');
}
return {
supabaseCookie,
metric: res,
};
},
});
function RouteComponent() {
const { version_number, type, width, height } = Route.useSearch();
const { user } = ScreenshotsRoute.useLoaderData();
const x = useGetUserBasicInfo();
return (
<div className="p-10 flex flex-col h-full border-red-500 border-10 items-center justify-center bg-blue-100 text-2xl text-blue-500">
<div> Hello "/screenshot/hello-world"!</div>
<div className="truncate max-w-[300px]">{x?.name}</div>
<div className="truncate max-w-[300px]">{user.accessToken}</div>
</div>
);
}

View File

@ -1,10 +1,8 @@
import { createServerFileRoute } from '@tanstack/react-start/server';
import { chromium } from 'playwright';
import { z } from 'zod';
import { getMetric } from '@/api/buster_rest/metrics';
import { env } from '@/env';
import { browserLogin } from '@/api/server-functions/screenshots/browser-login';
import { getSupabaseServerClient } from '@/integrations/supabase/server';
import { Route as MetricContentRoute } from './_content/metrics.$metricId.content';
import { createHrefFromLink } from '@/lib/routes';
const isDev = import.meta.env.DEV;
@ -37,63 +35,23 @@ export const ServerRoute = createServerFileRoute('/screenshots/metrics/$metricId
const { version_number, type, width, height } = GetMetricScreenshotQuerySchema.parse(
Object.fromEntries(new URL(request.url).searchParams)
);
const origin = new URL(request.url).origin;
// For Playwright, we need to reconstruct the session from the JWT
// Decode the JWT to get expiry time
const jwtPayload = JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString());
const { browser, page } = await browserLogin({
accessToken,
width,
height,
fullPath: createHrefFromLink({
to: '/screenshots/metrics/$metricId/content',
params: { metricId },
search: { version_number, type, width, height },
}),
request,
});
const session = {
access_token: accessToken,
token_type: 'bearer',
expires_in: 3600,
expires_at: jwtPayload.exp,
refresh_token: '',
user: user,
};
console.time('capture screenshot');
const browser = await chromium.launch();
console.timeLog('capture screenshot', 'browser launched');
try {
// Create browser context with authentication cookies
const context = await browser.newContext({
viewport: { width, height },
});
// Extract project ref from Supabase URL (e.g., "abcdefg" from "abcdefg.supabase.co")
const projectRef = '127';
// Format cookie value as Supabase expects: base64-<encoded_session>
const cookieValue = `base64-${Buffer.from(JSON.stringify(session)).toString('base64')}`;
console.log('Project ref:', projectRef);
console.log('Cookie name:', `sb-${projectRef}-auth-token`);
await context.addCookies([
{
name: `sb-${projectRef}-auth-token`,
value: cookieValue,
domain: new URL(env.VITE_PUBLIC_URL).hostname,
path: '/',
httpOnly: true,
secure: true,
sameSite: 'Lax',
},
]);
const page = await context.newPage();
console.timeLog('capture screenshot', 'page created with auth cookie');
const fullPath = `${origin}${MetricContentRoute.fullPath}`;
await page.goto(fullPath, { waitUntil: 'networkidle' });
console.timeLog('capture screenshot', 'page navigated');
const screenshotBuffer = await page.screenshot({
type,
});
console.timeLog('capture screenshot', 'screenshot taken');
console.timeEnd('capture screenshot');
if (!isDev) {
return new Response(
@ -116,7 +74,7 @@ export const ServerRoute = createServerFileRoute('/screenshots/metrics/$metricId
},
});
} catch (error) {
// console.error('Error capturing metric screenshot', error);
console.error('Error capturing metric screenshot', error);
return new Response(
JSON.stringify({
message: 'Failed to capture screenshot',