spb/toit-ka Public
Toit-Ka — louer ou acheter un toit au Québec, un seul endroit (fusion Lou-Ka × Immo-Ka) — www.toit-ka.com
Python 40.2%
TypeScript 39%
CSS 20.2%
HTML 0.7%
1# Author: Simon-Pierre Boucher2# Contact: contact@spboucher.ai3# Project: Toit-Ka4# -----------------------------------------------------------------------------5# hubprofile.py : profil membre lu depuis le HUB Groupe KA (groupe-ka.com).6# Le hub est LA source de vérité (bio, ville, emploi, réseaux, photo, statut)7# — l'édition se fait sur groupe-ka.com/compte, Toit·Ka ne fait qu'afficher.8# GET {hub}/api/sso/profile?client_id=toit-ka&ka_id=…&ts=…&sig=…9# avec sig = HMAC-SHA256(KA_SSO_SECRET, "toit-ka.<ka_id>.<ts>") en hex.10# Cache mémoire 60 s ; None sur toute erreur -> repli sur les données locales.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import hashlib15import hmac16import os17import threading18import time19from datetime import datetime, timezone2021import requests2223KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/")24CLIENT_ID = "toit-ka"25CACHE_TTL = 6026TIMEOUT = 52728_cache: dict[str, tuple[float, dict | None]] = {}29_lock = threading.Lock()303132def fetch_hub_profile(ka_id: str) -> dict | None:33 """Profil du membre au hub Groupe KA, ou None (inconnu ou injoignable)."""34 secret = os.environ.get("KA_SSO_SECRET")35 if not secret or not ka_id:36 return None37 now = time.time()38 with _lock:39 hit = _cache.get(ka_id)40 if hit and now - hit[0] < CACHE_TTL:41 return hit[1]42 data: dict | None = None43 try:44 ts = int(now)45 sig = hmac.new(secret.encode(), f"{CLIENT_ID}.{ka_id}.{ts}".encode(),46 hashlib.sha256).hexdigest()47 r = requests.get(48 f"{KA_HUB_URL}/api/sso/profile",49 params={"client_id": CLIENT_ID, "ka_id": ka_id, "ts": ts, "sig": sig},50 timeout=TIMEOUT)51 if r.status_code == 200:52 data = r.json()53 if not isinstance(data, dict):54 return None55 elif r.status_code != 404:56 return None # erreur transitoire (401, 5xx…) : pas de cache57 except Exception:58 return None # réseau/JSON : pas de cache59 with _lock:60 _cache[ka_id] = (now, data) # 200 -> data ; 404 -> None (négatif)61 return data626364def to_epoch(v) -> float | None:65 """created_at du hub (epoch OU chaîne ISO, UTC si naïve) -> epoch secondes."""66 if isinstance(v, (int, float)):67 return float(v)68 if isinstance(v, str) and v:69 try:70 dt = datetime.fromisoformat(v.replace("Z", "+00:00"))71 if dt.tzinfo is None:72 dt = dt.replace(tzinfo=timezone.utc)73 return dt.timestamp()74 except ValueError:75 return None76 return None77