Compare commits

...

1 Commits

Author SHA1 Message Date
qananasikq
d2dd17a02c fix env auth 2026-04-23 12:42:41 +03:00
3 changed files with 49 additions and 5 deletions

View File

@@ -2,6 +2,7 @@
OPENLANE_HEADLESS=true OPENLANE_HEADLESS=true
OPENLANE_LOG_LEVEL=INFO OPENLANE_LOG_LEVEL=INFO
OPENLANE_TIMEOUT_MS=45000 OPENLANE_TIMEOUT_MS=45000
OPENLANE_RUNTIME_CONFIG_FILE=runtime_config.json
# --- OpenLane Auth & API --- # --- OpenLane Auth & API ---
OPENLANE_USERNAME= OPENLANE_USERNAME=

View File

@@ -181,7 +181,7 @@ class Settings:
log_level: str = _env_str("OPENLANE_LOG_LEVEL", "INFO") log_level: str = _env_str("OPENLANE_LOG_LEVEL", "INFO")
log_file: str | None = _env_optional_str("OPENLANE_LOG_FILE") log_file: str | None = _env_optional_str("OPENLANE_LOG_FILE")
enable_trace_id_logs: bool = _env_bool("OPENLANE_ENABLE_TRACE_ID_LOGS", True) 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) fingerprint: FingerprintConfig = field(default_factory=FingerprintConfig)
openlane: OpenLaneConfig = field(default_factory=OpenLaneConfig) openlane: OpenLaneConfig = field(default_factory=OpenLaneConfig)
database: DatabaseConfig = field(default_factory=DatabaseConfig) database: DatabaseConfig = field(default_factory=DatabaseConfig)

View File

@@ -321,7 +321,9 @@ class OpenLaneAuthenticator:
"https://app.openlane.com/api/_next/time", "https://app.openlane.com/api/_next/time",
wait_until="domcontentloaded", 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 access_token без навигации.
refresh_token = self._extract_refresh_token_from_cookies(context) or self.config.refresh_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", "https://app.openlane.com/api/_next/time",
wait_until="domcontentloaded", wait_until="domcontentloaded",
) )
logger.info("OpenLane session restored via direct Okta refresh") if self._is_api_authenticated(page):
return 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") logger.warning("Direct refresh failed, trying sign_in fallback")
# Если refresh_token был только в env, добавляем cookie вручную. # Если refresh_token был только в env, добавляем cookie вручную.
@@ -355,7 +359,9 @@ class OpenLaneAuthenticator:
self._wait_for_authenticated_session(page) self._wait_for_authenticated_session(page)
self._persist_storage_state(context) self._persist_storage_state(context)
self._maybe_rotate_token(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: except OpenLaneAuthError:
logger.warning("SPA refresh_token exchange failed") logger.warning("SPA refresh_token exchange failed")
page.close() page.close()
@@ -370,6 +376,43 @@ class OpenLaneAuthenticator:
" 2. Set OPENLANE_USERNAME and OPENLANE_PASSWORD in .env" " 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: def login(self, context: BrowserContext) -> Page:
self.validate_credentials() self.validate_credentials()
page = context.new_page() page = context.new_page()