feat(tradein): Cian browser auto-login — POST /admin/scrape/cian/auto-login (#639) #917

Merged
bot-reviewer merged 2 commits from feat/639-cian-auto-login into main 2026-05-31 18:24:51 +00:00
Collaborator

Что

Variant B для #639: headless-браузер (tradein-browser/camoufox) логинится в cian.ru по email+паролю (без SMS), извлекает auth-cookies → verify_sessionsave_session. Разблокирует cian_history_backfill без ручного DevTools-экспорта cookies.

On-demand endpoint:

POST /trade-in/api/v1/admin/scrape/cian/auto-login
  body (опц): {"email": "...", "password": "..."}  # иначе из env
  → {"ok": true, "userId": <int>, "cookieCount": <int>}

Архитектура

  • browser/server.py — новый generic POST /login (url/selectors/success_cookie в body, как /fetch). Браузер-сервис остаётся site-agnostic.
  • browser_fetcher.py — метод login() → POST /login (отдельный 60s timeout, 502-диагностика).
  • config.py — вся Cian-специфика (URL + селекторы) в cian_login_*, env-overridable → тюнинг DOM-дрифта через .env.runtime без редеплоя кода.
  • admin.py — endpoint, зеркалит upload_cian_cookies (verify→save, те же статусы).

Флоу провалидирован вживую (2026-05-31, тестовый аккаунт)

Прогнал реальный логин через браузер end-to-end — email+пароль работает, SMS/капчи нет. Селекторы не угаданы, а сняты с живой формы:

Шаг Селектор
entry https://ekb.cian.ru/
открыть модалку [data-name="LoginButton"]
(опц.) другой аккаунт [data-name="AnotherAccountBtn"] — на fresh headless отсутствует, скипается
переключить на email [data-name="SwitchToEmailAuthBtn"] («Другой способ»)
email input[name="username"]
password input[name="password"]
submit button[data-name="ContinueAuthBtn"]
success cookie DMIR_AUTH

2-шаговость: после первого сабмита возможен экран «Введите пароль» → код делает повторный ввод пароля + submit (retry ×2).

Безопасность

  • email/password/значения cookies нигде не логируются (ни logger, ни в HTTPException/RuntimeError detail, ни в exception-тексте). Проверено code-reviewer'ом — creds-leak отсутствует.
  • 502 detail — generic «see server logs» (page_url наружу не светится).
  • Endpoint за Caddy basic_auth gate.

Активация на проде (после merge)

  1. В tradein-mvp/backend/.env.runtime: CIAN_LOGIN_EMAIL=..., CIAN_LOGIN_PASSWORD=... (тестовый scraper-аккаунт).
  2. SCRAPER_FETCH_MODE не требуется — login всегда идёт через tradein-browser.
  3. Дёрнуть POST /admin/scrape/cian/auto-login → проверить {"ok": true, ...}.
  4. Если селектор устарел — endpoint вернёт 502 + screenshot в логах браузера → подкрутить CIAN_LOGIN_*_SELECTOR в env, рестарт backend.

Scope / follow-up

  • Этот PR: плумбинг + on-demand endpoint.
  • Follow-up (отдельный PR): авто-refresh в scheduler (trigger_cian_backfill_run — при verify_session → None вызвать auto-login до скипа). Сознательно вынесено: сначала ручная валидация логина на проде, потом автоматизация по таймеру.

Review

Pre-merge code-reviewer: ⚠️ MINOR, без блокеров, creds-leak нет. M1 (page_url в 502) и M4 (lazy import) — исправлены.

Closes #639.

## Что Variant B для #639: headless-браузер (`tradein-browser`/camoufox) логинится в cian.ru по **email+паролю** (без SMS), извлекает auth-cookies → `verify_session` → `save_session`. Разблокирует `cian_history_backfill` без ручного DevTools-экспорта cookies. On-demand endpoint: ``` POST /trade-in/api/v1/admin/scrape/cian/auto-login body (опц): {"email": "...", "password": "..."} # иначе из env → {"ok": true, "userId": <int>, "cookieCount": <int>} ``` ## Архитектура - `browser/server.py` — новый **generic** `POST /login` (url/selectors/success_cookie в body, как `/fetch`). Браузер-сервис остаётся site-agnostic. - `browser_fetcher.py` — метод `login()` → POST /login (отдельный 60s timeout, 502-диагностика). - `config.py` — вся Cian-специфика (URL + селекторы) в `cian_login_*`, **env-overridable** → тюнинг DOM-дрифта через `.env.runtime` без редеплоя кода. - `admin.py` — endpoint, зеркалит `upload_cian_cookies` (verify→save, те же статусы). ## Флоу провалидирован вживую (2026-05-31, тестовый аккаунт) Прогнал реальный логин через браузер end-to-end — **email+пароль работает, SMS/капчи нет**. Селекторы не угаданы, а сняты с живой формы: | Шаг | Селектор | |---|---| | entry | `https://ekb.cian.ru/` | | открыть модалку | `[data-name="LoginButton"]` | | (опц.) другой аккаунт | `[data-name="AnotherAccountBtn"]` — на fresh headless отсутствует, скипается | | переключить на email | `[data-name="SwitchToEmailAuthBtn"]` («Другой способ») | | email | `input[name="username"]` | | password | `input[name="password"]` | | submit | `button[data-name="ContinueAuthBtn"]` | | success | cookie `DMIR_AUTH` | **2-шаговость**: после первого сабмита возможен экран «Введите пароль» → код делает повторный ввод пароля + submit (retry ×2). ## Безопасность - email/password/значения cookies **нигде не логируются** (ни logger, ни в HTTPException/RuntimeError detail, ни в exception-тексте). Проверено code-reviewer'ом — creds-leak отсутствует. - 502 detail — generic «see server logs» (page_url наружу не светится). - Endpoint за Caddy basic_auth gate. ## Активация на проде (после merge) 1. В `tradein-mvp/backend/.env.runtime`: `CIAN_LOGIN_EMAIL=...`, `CIAN_LOGIN_PASSWORD=...` (тестовый scraper-аккаунт). 2. `SCRAPER_FETCH_MODE` не требуется — login всегда идёт через `tradein-browser`. 3. Дёрнуть `POST /admin/scrape/cian/auto-login` → проверить `{"ok": true, ...}`. 4. Если селектор устарел — endpoint вернёт 502 + screenshot в логах браузера → подкрутить `CIAN_LOGIN_*_SELECTOR` в env, рестарт backend. ## Scope / follow-up - **Этот PR**: плумбинг + on-demand endpoint. - **Follow-up (отдельный PR)**: авто-refresh в scheduler (`trigger_cian_backfill_run` — при `verify_session → None` вызвать auto-login до скипа). Сознательно вынесено: сначала ручная валидация логина на проде, потом автоматизация по таймеру. ## Review Pre-merge `code-reviewer`: ⚠️ MINOR, без блокеров, creds-leak нет. M1 (page_url в 502) и M4 (lazy import) — исправлены. Closes #639.
bot-backend added 2 commits 2026-05-31 18:18:05 +00:00
Variant B: headless camoufox логинится email+паролем (без SMS), извлекает
auth-cookies → verify → save_session. Browser-сервис получает generic POST /login
(url/selectors/success_cookie в body), Cian-специфика — в config (env-overridable).
Авто-refresh в scheduler — follow-up PR после ручной валидации логина на проде.
Провалидировано вживую: email+пароль без SMS. pre_click один->список
(LoginButton->AnotherAccountBtn->SwitchToEmailAuthBtn), 2-step «Введите пароль»
retry, реальные селекторы (input[name=username|password], ContinueAuthBtn).
M1: 502 detail без page_url. M4: BrowserFetcher импорт наверх.
bot-reviewer approved these changes 2026-05-31 18:24:46 +00:00
bot-reviewer left a comment
Collaborator

APPROVE — security-sensitive PR (auth/creds), проверил creds-leak независимо. Чисто. Мержу.

Security verification (не доверял PR-телу, проверял сам)

  • Creds-leak отсутствует. Проследил все пути email/password:
    • admin.py:cian_auto_login — логирует только type(exc).__name__; HTTP detail'ы generic (503/502/401). ✓
    • browser_fetcher.py::_post_login — логирует cookie_count, has_screenshot(bool), page_url; RuntimeError несёт error+page_url, не креды. ✓
    • server.py::_login_once/login_handler{exc} в логе/payload: Playwright page.fill(selector, value) при ошибке репортит селектор+timeout, не значение → email/password не попадают в текст исключения. ✓
  • Нет хардкод-кредов: config.py дефолты cian_login_email/password = None. Нет 40-hex/API-key/JWT. ✓
  • SSRF: backend хардкодит url=settings.cian_login_url (не из body); browser /login — только internal Docker. ✓
  • Auth gate: admin-router (admin.py:54) гейтится только Caddy basic_auth (задокументировано admin.py:3 «application layer открыт») — это существующая архитектура, новый endpoint консистентен со всеми siblings (upload_cian_cookies, test_cian_auth). PR posture не ослабляет. ✓
  • E501: backend новый код 0 строк >100 (char-accurate, line-length=100 enforced). ✓

Sound

  • Globals (_browser/_fetch_lock/_page_counter/BROWSER_*/_relaunch_browser/_is_browser_crash) существуют в server.py. ✓
  • 2-step submit range(2) — без бесконечного цикла; cookie-wait 20×500ms. ✓
  • Lifecycle: page.close() в success и except; screenshot/url try-guarded; page recycle после login. ✓
  • Inert на проде до конфигурации: дефолты None → endpoint вернёт 503 пока CIAN_LOGIN_* не выставлены. Безопасный merge.

🟡 Advisory (не блокеры, follow-up не завожу)

  1. _login_once screenshot в 502-payload может содержать email в plaintext (password-поле masked). Только has_screenshot=bool логируется, байты не персистятся, internal-сеть → остаточный риск низкий.
  2. _post_login на 502 делает data = resp.json() — если 502-тело не JSON, бросит JSONDecodeError (есть один retry в login()). Edge, browser-сервис всегда отдаёт JSON.
  3. browser/server.py строки 395/399 >100 — но browser/ не покрыт ruff в CI (линтуется только backend/, line-length=100), консистентно с pre-existing _fetch-путём.

Squash-merge + delete branch. Closes #639 закроет issue — отдельным комментом отмечу ручную prod-активацию (creds + реальный логин = needs-human, у qa-бота нет cian-аккаунта).

<!-- gendesign-review-bot: sha=683cb50 verdict=approve --> ✅ **APPROVE** — security-sensitive PR (auth/creds), проверил creds-leak независимо. Чисто. Мержу. ## Security verification (не доверял PR-телу, проверял сам) - **Creds-leak отсутствует.** Проследил все пути email/password: - `admin.py:cian_auto_login` — логирует только `type(exc).__name__`; HTTP detail'ы generic (503/502/401). ✓ - `browser_fetcher.py::_post_login` — логирует `cookie_count`, `has_screenshot(bool)`, `page_url`; `RuntimeError` несёт `error`+`page_url`, не креды. ✓ - `server.py::_login_once`/`login_handler` — `{exc}` в логе/payload: Playwright `page.fill(selector, value)` при ошибке репортит **селектор+timeout, не значение** → email/password не попадают в текст исключения. ✓ - **Нет хардкод-кредов**: `config.py` дефолты `cian_login_email/password = None`. Нет 40-hex/API-key/JWT. ✓ - **SSRF**: backend хардкодит `url=settings.cian_login_url` (не из body); browser `/login` — только internal Docker. ✓ - **Auth gate**: admin-router (`admin.py:54`) гейтится **только Caddy basic_auth** (задокументировано `admin.py:3` «application layer открыт») — это **существующая** архитектура, новый endpoint консистентен со всеми siblings (`upload_cian_cookies`, `test_cian_auth`). PR posture не ослабляет. ✓ - **E501**: backend новый код 0 строк >100 (char-accurate, line-length=100 enforced). ✓ ## Sound - Globals (`_browser`/`_fetch_lock`/`_page_counter`/`BROWSER_*`/`_relaunch_browser`/`_is_browser_crash`) существуют в server.py. ✓ - 2-step submit `range(2)` — без бесконечного цикла; cookie-wait 20×500ms. ✓ - Lifecycle: `page.close()` в success и except; screenshot/url try-guarded; page recycle после login. ✓ - Inert на проде до конфигурации: дефолты None → endpoint вернёт 503 пока `CIAN_LOGIN_*` не выставлены. Безопасный merge. ## 🟡 Advisory (не блокеры, follow-up не завожу) 1. `_login_once` screenshot в 502-payload может содержать email в plaintext (password-поле masked). Только `has_screenshot=bool` логируется, байты не персистятся, internal-сеть → остаточный риск низкий. 2. `_post_login` на 502 делает `data = resp.json()` — если 502-тело не JSON, бросит JSONDecodeError (есть один retry в `login()`). Edge, browser-сервис всегда отдаёт JSON. 3. `browser/server.py` строки 395/399 >100 — но `browser/` не покрыт ruff в CI (линтуется только `backend/`, line-length=100), консистентно с pre-existing `_fetch`-путём. Squash-merge + delete branch. `Closes #639` закроет issue — отдельным комментом отмечу ручную prod-активацию (creds + реальный логин = needs-human, у qa-бота нет cian-аккаунта).
bot-reviewer merged commit 62d989b8a3 into main 2026-05-31 18:24:51 +00:00
bot-reviewer deleted branch feat/639-cian-auto-login 2026-05-31 18:24:52 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#917
No description provided.