""" common.py — shared auth helpers для farmOS tools. farmOS 4.x підтримує тільки OAuth2 Bearer (не Basic Auth). При наявності FARMOS_TOKEN — використовуємо його як статичний Bearer. При наявності FARMOS_USER + FARMOS_PASS — отримуємо JWT через OAuth2 password grant і кешуємо його в пам'яті до закінчення TTL. """ import os import time import threading import requests # ── In-memory OAuth2 token cache (process-level, thread-safe) ──────────────── _token_cache: dict = {} _token_lock = threading.Lock() _OAUTH_TOKEN_PATH = "/oauth/token" # Оновлюємо токен за 60 секунд до закінчення, щоб не отримати 401 _TOKEN_REFRESH_BUFFER_S = 60 def _farmos_base_url() -> str: return os.getenv("FARMOS_BASE_URL", "").strip().rstrip("/") def _fetch_oauth_token(base_url: str, user: str, pwd: str, client_id: str) -> str | None: """ Робить POST /oauth/token з password grant. Fail-closed: будь-яка помилка → повертає None (без raise). """ try: resp = requests.post( base_url + _OAUTH_TOKEN_PATH, data={ "grant_type": "password", "username": user, "password": pwd, "client_id": client_id, "scope": "", }, timeout=5, ) if resp.status_code == 200: data = resp.json() access_token = data.get("access_token", "") expires_in = float(data.get("expires_in", 3600)) if access_token: with _token_lock: _token_cache["access_token"] = access_token _token_cache["expires_at"] = time.monotonic() + expires_in - _TOKEN_REFRESH_BUFFER_S return access_token except Exception: pass return None def _get_cached_oauth_token(base_url: str, user: str, pwd: str, client_id: str) -> str | None: """ Повертає кешований токен або отримує новий якщо протух / відсутній. """ with _token_lock: cached = _token_cache.get("access_token") expires_at = _token_cache.get("expires_at", 0.0) if cached and time.monotonic() < expires_at: return cached # Поза lock — робимо мережевий запит return _fetch_oauth_token(base_url, user, pwd, client_id) def _auth_headers() -> dict: """ Повертає заголовки авторизації для farmOS JSON:API запитів. Пріоритет: 1. FARMOS_TOKEN (статичний Bearer — для dev/тестів або PAT) 2. FARMOS_USER + FARMOS_PASS → OAuth2 password grant → Bearer JWT 3. Порожній dict (fail-closed: запит піде без auth, farmOS поверне 403/401) """ # 1. Статичний токен token = os.getenv("FARMOS_TOKEN", "").strip() if token: return {"Authorization": f"Bearer {token}"} # 2. OAuth2 password grant user = os.getenv("FARMOS_USER", "").strip() pwd = os.getenv("FARMOS_PASS", os.getenv("FARMOS_PASSWORD", "")).strip() client_id = os.getenv("FARMOS_CLIENT_ID", "farm").strip() base_url = _farmos_base_url() if user and pwd and base_url: jwt = _get_cached_oauth_token(base_url, user, pwd, client_id) if jwt: return {"Authorization": f"Bearer {jwt}"} return {}