Files
portal/app/playwright/refresh-session.js
T
Дмитрий e8e5c82b86
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
fix(приёмка): FN-RESET + FN-LOGIN-ROUTE + диагностируемость FN-SESSION
FN-RESET: письмо сброса строило именованный роут password.reset которого нет в SPA.
ResetPassword::createUrlUsing → /reset/{token}?email= в AppServiceProvider boot.

FN-LOGIN-ROUTE: гость без Accept json на auth:sanctum уводил в именованный роут
login которого нет → 500. redirectGuestsTo /login + render AuthenticationException
→ 401 JSON для api/*.

FN-SESSION: chromium.launch стоял вне try/catch — отказ запуска браузера маскировался
unhandled-rejection в opaque exit 1 двойник login-rejected. launch в try + top-level
catch → чистый exit 4 + JSON stderr в refresh-session.js и manage-project.js.

Тесты: PasswordResetUrlTest, UnauthenticatedApiResponseTest, node:test launch-failure
в обоих playwright-скриптах. Разбор FN-SESSION + ops-долг playwright install под
www-data + поправки отчёта приёмки + новая находка FN-INN-LOOKUP.

Прод не трогался. Накат — позже вместе с остальным.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 08:49:19 +03:00

124 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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) {
// browser.launch ВНУТРИ try: отказ запуска (нет исполняемого файла браузера,
// sandbox, нехватка libs) должен классифицироваться как exit 4 + JSON stderr,
// а не уходить unhandled-rejection'ом в опасный exit 1 (FN-SESSION, приёмка 22.06).
let browser = null;
try {
browser = await chromium.launch({ headless: true });
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 {
if (browser) {
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);
}
// Top-level guard: любая rejection, не пойманная внутри refresh(), всё равно
// даёт чистый exit 4 + JSON stderr, а не unhandled-rejection exit 1.
refresh(args).catch((err) => {
const message = err && err.message ? err.message : String(err);
process.stderr.write(JSON.stringify({ error: message }));
process.exit(4);
});
});