mirror of https://github.com/buster-so/buster.git
add browser login path
This commit is contained in:
parent
065ec68ab0
commit
f47891003e
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue