Make vide work with prod

This commit is contained in:
Andras Schmelczer 2026-05-10 14:55:53 +01:00
parent 3debacab4f
commit ee231d2ee5
7 changed files with 197 additions and 67 deletions

View file

@ -6,63 +6,56 @@ import { ensureRecorderAdminUser } from './pb-admin.js';
* Auth setup. Two modes:
*
* 1. Programmatic (preferred for CI / non-interactive runs): set
* PB_URL, PB_EMAIL, PB_PASSWORD env vars. We hit the PocketBase REST
* auth-with-password endpoint, then hand-write a Playwright storageState
* file with the resulting token in localStorage["pb_auth"]. The PocketBase
* JS SDK reads that key on boot and treats us as logged in bit-equivalent
* to a real UI login.
* 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.
*/
interface PbAuthResponse {
token: string;
record: Record<string, unknown>;
}
async function programmatic() {
const email = process.env.PB_EMAIL!;
const password = process.env.PB_PASSWORD!;
const email = process.env.LOGIN_EMAIL!;
const password = process.env.LOGIN_PASSWORD!;
if (process.env.PB_BOOTSTRAP_ADMIN !== '0') {
await ensureRecorderAdminUser();
}
// Driving the login through the app itself ensures the PocketBase SDK's
// LocalAuthStore sees the token via its own write path. Hand-writing
// localStorage["pb_auth"] sometimes races with the SDK's module-time read.
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);
await page.evaluate(
async ({ email, password }) => {
const res = await fetch('/pb/api/collections/users/auth-with-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: email, password }),
});
if (!res.ok) throw new Error(`login ${res.status} ${await res.text()}`);
const data = await res.json();
// The SDK's LocalAuthStore default storageKey is "pocketbase_auth",
// not "pb_auth" (which is just the cookie name in BaseAuthStore).
localStorage.setItem(
'pocketbase_auth',
JSON.stringify({ token: data.token, record: data.record })
);
// Skip the react-joyride product tour — its spotlight overlay
// intercepts pointer events and breaks the recording.
localStorage.setItem('tutorial_completed', '1');
// 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; }
},
{ email, password }
{ 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 in-app PocketBase login (user: ${email}).`);
console.log(`Saved ${AUTH_STATE_PATH} via dashboard login form (user: ${email}).`);
}
async function interactive() {
@ -88,7 +81,7 @@ async function interactive() {
}
async function main() {
if (process.env.PB_URL && process.env.PB_EMAIL && process.env.PB_PASSWORD) {
if (process.env.LOGIN_EMAIL && process.env.LOGIN_PASSWORD) {
await programmatic();
process.exit(0);
}

View file

@ -17,7 +17,9 @@ function requiredNumberEnv(name: string): number {
export const APP_URL = requiredEnv("APP_URL");
export const DASHBOARD_PATH = "/dashboard";
export const AUTH_STATE_PATH = "auth.json";
// Per-target storage state. render.sh sets AUTH_STATE_FILE to auth.local.json
// or auth.prod.json so a stale local token can't be reused against prod.
export const AUTH_STATE_PATH = process.env.AUTH_STATE_FILE ?? "auth.json";
export const OUTPUT_DIR = "output";
const aspect = requiredEnv("ASPECT");

View file

@ -64,8 +64,8 @@ function recorderUserBody(email: string, password: string): Record<string, unkno
export async function ensureRecorderAdminUser(): Promise<void> {
const pbUrl = requireEnv('PB_URL').replace(/\/$/, '');
const email = requireEnv('PB_EMAIL');
const password = requireEnv('PB_PASSWORD');
const email = requireEnv('LOGIN_EMAIL');
const password = requireEnv('LOGIN_PASSWORD');
const adminEmail = process.env.PB_ADMIN_EMAIL ?? process.env.POCKETBASE_ADMIN_EMAIL;
const adminPassword = process.env.PB_ADMIN_PASSWORD ?? process.env.POCKETBASE_ADMIN_PASSWORD;