Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.
TypeScript 90.2%
JavaScript 3.5%
Python 3.4%
CSS 1.9%
HTML 0.6%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// -----------------------------------------------------------------------------3// Profil membre lu depuis le HUB Groupe KA (groupe-ka.com).4// Le hub est LA source de vérité du profil (bio, ville, emploi, entreprise,5// site web, réseaux sociaux, photo, statut, profil public) — l'édition se6// fait sur groupe-ka.com/compte, Vrai-Prix ne fait qu'afficher.7// GET {hub}/api/sso/profile?client_id=vrai-prix&ka_id=…&ts=…&sig=…8// avec sig = HMAC-SHA256(KA_SSO_SECRET, "vrai-prix.<ka_id>.<ts>") en hex9// (même secret que le SSO). Cache mémoire 60 s (les 404 aussi — cache10// négatif) ; null sur toute erreur (réseau, 401, 5xx, vieux compte non11// relié) → l'appelant retombe sur les données locales de session.12// Aucune dépendance : node:crypto + fetch natif seulement.13// -----------------------------------------------------------------------------14import { createHmac } from "node:crypto";15import { CLIENT_ID, KA_HUB_URL } from "@/lib/ka-auth";1617const CACHE_TTL_MS = 60_000; // 60 s18const TIMEOUT_MS = 5_000; // 5 s1920export type HubSocials = {21 instagram?: string;22 facebook?: string;23 x?: string;24 linkedin?: string;25 tiktok?: string;26 youtube?: string;27};2829/** Réponse de GET /api/sso/profile du hub (200). */30export type HubProfile = {31 ka_id: string;32 name: string;33 email: string;34 picture: string;35 role: string;36 role_label: string;37 bio: string;38 city: string;39 phone: string;40 website: string;41 job_title: string;42 company: string;43 birth_date: string;44 age: number | null;45 socials: HubSocials;46 public: boolean;47 public_url: string;48 created_at: string | number;49};5051// Cache mémoire process-wide (Map globale — survit au HMR en dev) :52// 200 → profil ; 404 → null (cache négatif). Erreurs transitoires : pas de cache.53type CacheEntry = { at: number; data: HubProfile | null };54const g = globalThis as unknown as { __vpHubProfileCache?: Map<string, CacheEntry> };55const cache: Map<string, CacheEntry> =56 g.__vpHubProfileCache ?? (g.__vpHubProfileCache = new Map());5758/** Profil du membre au hub Groupe KA, ou null (inconnu ou injoignable). */59export async function fetchHubProfile(kaId: string): Promise<HubProfile | null> {60 const secret = process.env.KA_SSO_SECRET;61 if (!secret || !kaId) return null;6263 const now = Date.now();64 const hit = cache.get(kaId);65 if (hit && now - hit.at < CACHE_TTL_MS) return hit.data;6667 const ts = Math.floor(now / 1000);68 const sig = createHmac("sha256", secret)69 .update(`${CLIENT_ID}.${kaId}.${ts}`)70 .digest("hex");71 const url =72 `${KA_HUB_URL}/api/sso/profile?client_id=${encodeURIComponent(CLIENT_ID)}` +73 `&ka_id=${encodeURIComponent(kaId)}&ts=${ts}&sig=${sig}`;7475 const ctrl = new AbortController();76 const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);77 try {78 const r = await fetch(url, { signal: ctrl.signal, cache: "no-store" });79 if (r.status === 404) {80 cache.set(kaId, { at: now, data: null }); // cache négatif 60 s81 return null;82 }83 if (!r.ok) return null; // transitoire (401, 5xx…) : pas de cache84 const data = (await r.json()) as unknown;85 if (typeof data !== "object" || data === null) return null;86 const profile = data as HubProfile;87 cache.set(kaId, { at: now, data: profile });88 return profile;89 } catch {90 return null; // réseau / timeout / JSON : pas de cache91 } finally {92 clearTimeout(timer);93 }94}95