diff --git a/.env.example b/.env.example index da46cec..9d18476 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ OPENLANE_HEADLESS=true OPENLANE_LOG_LEVEL=INFO OPENLANE_TIMEOUT_MS=45000 +OPENLANE_RUNTIME_CONFIG_FILE=runtime_config.json # --- OpenLane Auth & API --- OPENLANE_USERNAME= diff --git a/openlane_scraper/core/config.py b/openlane_scraper/core/config.py index fdac325..3c4ca1f 100644 --- a/openlane_scraper/core/config.py +++ b/openlane_scraper/core/config.py @@ -181,7 +181,7 @@ class Settings: log_level: str = _env_str("OPENLANE_LOG_LEVEL", "INFO") log_file: str | None = _env_optional_str("OPENLANE_LOG_FILE") enable_trace_id_logs: bool = _env_bool("OPENLANE_ENABLE_TRACE_ID_LOGS", True) - runtime_config_file: str | None = _env_path_str("OPENLANE_RUNTIME_CONFIG_FILE") + runtime_config_file: str | None = _env_path_str("OPENLANE_RUNTIME_CONFIG_FILE") or "runtime_config.json" fingerprint: FingerprintConfig = field(default_factory=FingerprintConfig) openlane: OpenLaneConfig = field(default_factory=OpenLaneConfig) database: DatabaseConfig = field(default_factory=DatabaseConfig) diff --git a/openlane_scraper/openlane/auth.py b/openlane_scraper/openlane/auth.py index 3526c28..5ab6bf7 100644 --- a/openlane_scraper/openlane/auth.py +++ b/openlane_scraper/openlane/auth.py @@ -321,7 +321,9 @@ class OpenLaneAuthenticator: "https://app.openlane.com/api/_next/time", wait_until="domcontentloaded", ) - return page + if self._is_api_authenticated(page): + return page + logger.warning("Access token cookie exists but API returned unauthorized, will re-authenticate") # Пробуем прямой refresh access_token без навигации. refresh_token = self._extract_refresh_token_from_cookies(context) or self.config.refresh_token @@ -337,8 +339,10 @@ class OpenLaneAuthenticator: "https://app.openlane.com/api/_next/time", wait_until="domcontentloaded", ) - logger.info("OpenLane session restored via direct Okta refresh") - return page + if self._is_api_authenticated(page): + logger.info("OpenLane session restored via direct Okta refresh") + return page + logger.warning("Direct Okta refresh produced token but API still unauthorized") logger.warning("Direct refresh failed, trying sign_in fallback") # Если refresh_token был только в env, добавляем cookie вручную. @@ -355,7 +359,9 @@ class OpenLaneAuthenticator: self._wait_for_authenticated_session(page) self._persist_storage_state(context) self._maybe_rotate_token(context) - return page + if self._is_api_authenticated(page): + return page + logger.warning("sign_in fallback completed but API is still unauthorized") except OpenLaneAuthError: logger.warning("SPA refresh_token exchange failed") page.close() @@ -370,6 +376,43 @@ class OpenLaneAuthenticator: " 2. Set OPENLANE_USERNAME and OPENLANE_PASSWORD in .env" ) + def _is_api_authenticated(self, page: Page) -> bool: + """Проверяет, что сессия реально авторизована для OpenLane API. + + Нужна для случаев, когда access_token cookie есть (и даже не истёк), + но сервер уже считает сессию невалидной. + """ + try: + status = page.evaluate( + """async () => { + try { + const resp = await fetch('https://app.openlane.com/api/v4/search?page=1&per_page=1&source_tab=marketplace&sale_types=marketplace', { + method: 'GET', + credentials: 'include', + headers: { + 'Accept': 'application/json, application/vnd.backlotcars.v3' + } + }); + return resp.status; + } catch (e) { + return 0; + } + }""" + ) + except Exception: + logger.warning("Failed to validate OpenLane API auth state", exc_info=True) + return False + + try: + code = int(status) + except (TypeError, ValueError): + return False + + # 401/403 — точно неавторизованы. + if code in (401, 403, 0): + return False + return True + def login(self, context: BrowserContext) -> Page: self.validate_credentials() page = context.new_page()