import { chromium } from 'playwright'; import { APP_URL, AUTH_STATE_PATH } from './config.js'; import { ensureRecorderAdminUser } from './pb-admin.js'; /** * Auth setup. Two modes: * * 1. Programmatic (preferred for CI / non-interactive runs): set * LOGIN_EMAIL and LOGIN_PASSWORD env vars (the same email/password you'd * type into the dashboard's login modal). We drive the actual login form * in a headless browser — same path a real user takes, no knowledge of * the PocketBase REST endpoint required. * * 2. Interactive: no env vars, we open a headed browser, you log in by hand, * press Enter, and we serialize the resulting cookies + localStorage. * Works on a developer laptop; doesn't work in headless environments. */ async function programmatic() { const email = process.env.LOGIN_EMAIL!; const password = process.env.LOGIN_PASSWORD!; if (process.env.PB_BOOTSTRAP_ADMIN !== '0') { await ensureRecorderAdminUser(); } const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1280, height: 800 } }); const page = await context.newPage(); await page.goto(APP_URL); // Drive the dashboard's actual login modal. Scoping to
avoids the // modal's own "Log in" submit button (same text, different element). await page.locator('header').getByRole('button', { name: /log in/i }).click(); await page.locator('input[type="email"]').fill(email); const pw = page.locator('input[type="password"]'); await pw.fill(password); await pw.press('Enter'); // Settle: the SDK writes localStorage["pocketbase_auth"] once auth-with- // password resolves. Polling localStorage is more robust than waiting on // a UI signal (modal close + user-menu render varies by viewport/state). await page.waitForFunction( () => { const v = localStorage.getItem('pocketbase_auth'); if (!v) return false; try { return !!(JSON.parse(v) as { token?: string }).token; } catch { return false; } }, { timeout: 15_000 } ); // Skip the react-joyride product tour — its spotlight overlay intercepts // pointer events and breaks the recording. await page.evaluate(() => { localStorage.setItem('tutorial_completed', '1'); }); await context.storageState({ path: AUTH_STATE_PATH }); await browser.close(); console.log(`Saved ${AUTH_STATE_PATH} via dashboard login form (user: ${email}).`); } async function interactive() { const browser = await chromium.launch({ headless: false }); const context = await browser.newContext({ viewport: { width: 1280, height: 800 } }); const page = await context.newPage(); await page.goto(APP_URL); console.log(''); console.log(' → Log in via the UI in the opened browser window.'); console.log(' → Once you see the dashboard, press Enter in this terminal.'); console.log(''); await new Promise((resolve) => { process.stdin.resume(); process.stdin.once('data', () => resolve()); }); await context.storageState({ path: AUTH_STATE_PATH }); console.log(`Saved storage state to ${AUTH_STATE_PATH}`); await browser.close(); process.exit(0); } async function main() { if (process.env.LOGIN_EMAIL && process.env.LOGIN_PASSWORD) { await programmatic(); process.exit(0); } await interactive(); } main().catch((err) => { console.error(err); process.exit(1); });