# ----------------------------------------------------------------------------- # Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # hubfav.py : favoris unifiés « Mon univers Ka » — le hub Groupe KA # (groupe-ka.com) est le MAGASIN CENTRAL des favoris du groupe : Auto·Ka ne # stocke RIEN localement, chaque ♥ est poussé (POST signé, synchrone) et # relu (GET signé) au hub. Signature HMAC-SHA256 du secret SSO partagé : # sig = HMAC(KA_SSO_SECRET, "auto-ka..") en hex, ts ±5 min côté # hub. Lecture avec cache mémoire 30 s (invalidé à chaque toggle) ; liste # vide sur toute erreur — le hub reste la seule source de vérité. # Config .env : KA_SSO_SECRET, KA_HUB_URL (optionnel). # ----------------------------------------------------------------------------- from __future__ import annotations import hashlib import hmac import os import threading import time import requests CLIENT_ID = "auto-ka" KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/") CACHE_TTL = 30 # secondes — lecture des favoris TIMEOUT = 6 # secondes — chaque appel au hub _cache: dict[str, tuple[float, list[dict]]] = {} _lock = threading.Lock() def _sig(ka_id: str, ts: int) -> str | None: secret = os.environ.get("KA_SSO_SECRET") if not secret: return None return hmac.new(secret.encode(), f"{CLIENT_ID}.{ka_id}.{ts}".encode(), hashlib.sha256).hexdigest() def invalidate(ka_id: str) -> None: """Oublie le cache de lecture de ce membre (après un toggle).""" with _lock: _cache.pop(ka_id, None) def hub_toggle(ka_id: str, action: str, item: dict) -> bool: """Pousse un ♥ au hub (action « add » ou « remove »), en SYNCHRONE : True si le hub confirme, False sinon (réseau, signature, membre inconnu). Le cache de lecture du membre est invalidé dans tous les cas.""" ts = int(time.time()) sig = _sig(ka_id, ts) if not sig or not ka_id: return False ok = False try: r = requests.post(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT, json={"client_id": CLIENT_ID, "ka_id": ka_id, "ts": str(ts), "sig": sig, "action": action, "item": item}) ok = r.status_code == 200 except Exception: ok = False invalidate(ka_id) return ok def hub_list(ka_id: str) -> list[dict]: """Favoris Auto·Ka du membre, lus au hub (cache 30 s, [] sur erreur).""" ts_now = time.time() with _lock: hit = _cache.get(ka_id) if hit and ts_now - hit[0] < CACHE_TTL: return hit[1] sig = _sig(ka_id, int(ts_now)) if not sig or not ka_id: return [] try: r = requests.get(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT, params={"client_id": CLIENT_ID, "ka_id": ka_id, "ts": int(ts_now), "sig": sig}) if r.status_code != 200: return [] favs = r.json().get("favorites") if not isinstance(favs, list): return [] except Exception: return [] with _lock: _cache[ka_id] = (ts_now, favs) return favs