94 lines
3.3 KiB
TypeScript
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);
|
|
});
|