perfect-postcode/video/src/auth.ts

94 lines
3.3 KiB
TypeScript

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 <header> 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<void>((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);
});