import { chromium } from 'playwright'; import fs from 'fs'; import path from 'path'; const DIR = process.env.STATE_DIR || '/data'; const COOKIE_FILE = path.join(DIR, 'state.json'); const LOG_DIR = path.join(DIR, 'log'); /** * Launch Playwright, sign in to n8n if needed, refresh cookie, * save a screenshot, return status object. */ export async function runLogin() { let browser; try { /* ---------- ensure writable paths exist ---------- */ fs.mkdirSync(LOG_DIR, { recursive: true }); // also creates DIR browser = await chromium.launch({ args: ['--no-sandbox'] }); /* ---------- reuse cookie ---------- */ const hasCookie = fs.existsSync(COOKIE_FILE); const context = await browser.newContext({ storageState: hasCookie ? COOKIE_FILE : undefined, ignoreHTTPSErrors: true, // in case your n8n URL uses self-signed cert }); const page = await context.newPage(); const baseUrl = process.env.N8N_URL || 'http://localhost:5678/'; await page.goto(baseUrl, { waitUntil: 'networkidle', timeout: 60000 }); /* ---------- BASIC AUTH (optional) ---------- */ const basicUserInput = page.locator('input[type="text"]'); if (process.env.BASIC_USER && await basicUserInput.count()) { await basicUserInput.fill(process.env.BASIC_USER); await page.fill('input[type="password"]', process.env.BASIC_PASS || ''); await page.press('input[type="password"]', 'Enter'); } /* ---------- detect current page ---------- */ const signInForm = page.locator('[data-test-id="signin-form"]'); const overviewTab = page.getByRole('link', { name: /^overview$/i }); let needSignIn = false; try { await Promise.race([ signInForm.waitFor({ state: 'visible', timeout: 45000 }), overviewTab.waitFor({ state: 'visible', timeout: 45000 }), ]); needSignIn = await signInForm.count() > 0; } catch (_) { needSignIn = false; // neither selector appeared } /* ---------- perform sign-in ---------- */ if (needSignIn) { const email = page.locator( 'input[name="emailOrLdapLoginId"], input[name="email"]' ).first(); const pass = page.locator('input[name="password"]').first(); await email.fill(process.env.N8N_EMAIL || ''); await pass.fill(process.env.N8N_PASSWORD || ''); await page.getByRole('button', { name: /sign in/i }).click(); await overviewTab.waitFor({ state: 'visible', timeout: 60000 }); } /* save cookie */ await context.storageState({ path: COOKIE_FILE }); const ts = new Date().toISOString().replace(/[:T]/g, '-').split('.')[0]; const shot = path.join(LOG_DIR, `overview-${ts}.png`); await page.screenshot({ path: shot, fullPage: true }); await context.tracing.stop({ path: path.join(DIR, 'trace.zip') }); return { ok: true, screenshot: shot, loggedIn: needSignIn ? 'fresh' : 'cookie' }; } catch (err) { console.error('❌ runLogin error:', err); return { ok: false, error: err.message }; } finally { if (browser) await browser.close(); } }