#!/usr/bin/env node /** * Headless Playwright login на crm.bp-gr.ru. * * Input (JSON через stdin): * {login, password, url} * * Output (JSON через stdout): * {phpsessid, csrf, refreshed_at} * * Exit codes: * 0 — success * 1 — auth failed (login/password rejected, или session cookie missing) * 2 — DOM не найден (CSRF token не найден) * 3 — timeout (60s) * 4 — invalid input или другая ошибка */ const { chromium } = require('playwright'); const TIMEOUT_MS = 60_000; async function refresh(args) { const browser = await chromium.launch({ headless: true }); try { const context = await browser.newContext(); const page = await context.newPage(); await page.goto(args.url, { waitUntil: 'load', timeout: TIMEOUT_MS }); // DOM-селекторы crm.bp-gr.ru/login (Yii2 LoginForm) — verified live 2026-05-19 через Playwright MCP. const loginSelector = '#loginform-username'; const passwordSelector = '#loginform-password'; const submitSelector = 'button[type=submit]'; await page.fill(loginSelector, args.login); await page.fill(passwordSelector, args.password); // Сабмит + ОЖИДАНИЕ пост-логин перехода. // Старый Promise.all([waitForLoadState('networkidle'), click]) — гонка: // логин-страница уже в состоянии networkidle, поэтому waitForLoadState // резолвился мгновенно (ДО редиректа), и скрипт хватал PHPSESSID // неаутентифицированной логин-страницы. Ждём, пока логин-форма исчезнет // из DOM — waitForFunction опрашивает и переживает навигацию. await page.click(submitSelector); await page .waitForFunction( (sel) => !document.querySelector(sel), loginSelector, { timeout: TIMEOUT_MS }, ) .catch(() => { /* форма осталась — логин отклонён, ловится guard'ом ниже */ }); await page.waitForLoadState('networkidle', { timeout: TIMEOUT_MS }).catch(() => {}); // Verify: логин-форма всё ещё на странице → вход НЕ удался. Не возвращаем // мусорную (неаутентифицированную) сессию как «успех» (exit 0). if ((await page.locator(loginSelector).count()) > 0) { process.stderr.write(JSON.stringify({ error: 'login rejected: still on login page after submit' })); process.exit(1); } let csrf = null; try { csrf = await page.locator('meta[name=csrf-token]').first().getAttribute('content', { timeout: 5000 }); } catch (e) { // CSRF meta tag not found — try other patterns в Task 1 discovery } const cookies = await context.cookies(); const sessionCookie = cookies.find(c => c.name === 'PHPSESSID' || c.name === 'JSESSIONID'); if (!sessionCookie) { process.stderr.write(JSON.stringify({ error: 'session cookie not found in response' })); process.exit(1); } if (!csrf) { process.stderr.write(JSON.stringify({ error: 'CSRF token not found in DOM' })); process.exit(2); } process.stdout.write(JSON.stringify({ phpsessid: sessionCookie.value, csrf: csrf, refreshed_at: new Date().toISOString(), })); process.exit(0); } catch (err) { process.stderr.write(JSON.stringify({ error: err.message })); process.exit(err.message.includes('Timeout') ? 3 : 4); } finally { await browser.close(); } } // Read stdin let input = ''; process.stdin.on('data', chunk => { input += chunk; }); process.stdin.on('end', () => { let args; try { args = JSON.parse(input); } catch (e) { process.stderr.write(JSON.stringify({ error: 'invalid JSON on stdin' })); process.exit(4); } if (!args.login || !args.password || !args.url) { process.stderr.write(JSON.stringify({ error: 'missing required keys: login, password, url' })); process.exit(4); } refresh(args); });