""" Obtención de un access token de Keycloak VÁLIDO para llamar a la API del Hub. Con el patrón de sesión local (SIWEB) el Bearer de la app es un JWT propio (HS256) que el Hub NO entiende. Para las llamadas server→Hub se usa el token KC guardado en la sesión (valkey, vía cookie crm_sid), refrescándolo si está por expirar. """ import logging import time from typing import Optional import httpx from jose import jwt from core.config import settings from core import session_store logger = logging.getLogger(__name__) def _kc_exp_ok(token: str, leeway_seconds: int = 30) -> bool: """True si el token KC no está expirado (con margen).""" try: claims = jwt.get_unverified_claims(token) exp = claims.get("exp") return isinstance(exp, (int, float)) and (int(exp) - int(time.time())) > leeway_seconds except Exception: return False async def get_hub_access_token(request) -> Optional[str]: """ Devuelve un access token KC válido tomado de la sesión (valkey vía crm_sid), refrescándolo contra el Hub si está por expirar. None si no hay sesión. Best-effort: si el refresh falla, devuelve el token guardado (puede estar vencido). """ sid = request.cookies.get("crm_sid") if request is not None else None if not sid: return None sess = session_store.get_session(sid) if not sess: return None access = sess.get("access_token") refresh = sess.get("refresh_token") if access and _kc_exp_ok(access): return access if refresh: try: async with httpx.AsyncClient(timeout=8.0) as client: r = await client.post( f"{settings.HUB_URL}api/v1/auth/refresh", json={"refresh_token": refresh}, ) if r.status_code == 200: data = r.json() new_access = data.get("access_token") or access session_store.update_session_tokens(sid, new_access, data.get("refresh_token") or refresh) return new_access logger.info("get_hub_access_token: Hub refresh devolvió %s", r.status_code) except Exception as exc: logger.warning("get_hub_access_token: refresh falló: %s", exc) return access