// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // Favoris unifiés « Mon univers Ka » — API serveur-à-serveur. // Le hub est LE magasin central : les plateformes poussent (POST) et // lisent (GET) les favoris de leurs membres, signés du secret SSO partagé. // POST { client_id, ka_id, ts, sig, action: "add"|"remove", // item: { item_id, title, subtitle?, price_label?, image_url?, // url?, meta? } } // GET ?client_id&ka_id&ts&sig[&scope=app|all] (défaut: app) import { NextRequest, NextResponse } from "next/server"; import { findUserByKaId, upsertFavorite, removeFavorite, favoritesOf, } from "@/lib/db"; import { verifyClientSig } from "@/lib/sso"; const clip = (v: unknown, max: number): string => typeof v === "string" ? v.trim().slice(0, max) : ""; export async function POST(req: NextRequest) { const body = (await req.json().catch(() => ({}))) as Record; const clientId = clip(body.client_id, 30); const kaId = clip(body.ka_id, 20); if (!verifyClientSig(clientId, kaId, String(body.ts ?? ""), String(body.sig ?? ""))) return NextResponse.json({ error: "signature invalide" }, { status: 401 }); const user = findUserByKaId(kaId); if (!user) return NextResponse.json({ error: "membre introuvable" }, { status: 404 }); const item = (body.item ?? {}) as Record; const itemId = clip(item.item_id, 200); if (!itemId) return NextResponse.json({ error: "item_id requis" }, { status: 400 }); if (body.action === "remove") { removeFavorite(user.id, clientId, itemId); return NextResponse.json({ ok: true, action: "remove" }); } const title = clip(item.title, 200); if (!title) return NextResponse.json({ error: "title requis" }, { status: 400 }); upsertFavorite(user.id, clientId, { item_id: itemId, title, subtitle: clip(item.subtitle, 200) || undefined, price_label: clip(item.price_label, 60) || undefined, image_url: clip(item.image_url, 500) || undefined, url: clip(item.url, 500) || undefined, meta: item.meta, }); return NextResponse.json({ ok: true, action: "add" }); } export async function GET(req: NextRequest) { const q = req.nextUrl.searchParams; const clientId = q.get("client_id") ?? ""; const kaId = q.get("ka_id") ?? ""; if (!verifyClientSig(clientId, kaId, q.get("ts") ?? "", q.get("sig") ?? "")) return NextResponse.json({ error: "signature invalide" }, { status: 401 }); const user = findUserByKaId(kaId); if (!user) return NextResponse.json({ error: "membre introuvable" }, { status: 404 }); const scope = q.get("scope") === "all" ? undefined : clientId; const rows = favoritesOf(user.id, scope); return NextResponse.json({ favorites: rows.map((r) => ({ app: r.app, item_id: r.item_id, title: r.title, subtitle: r.subtitle, price_label: r.price_label, image_url: r.image_url, url: r.url, created_at: r.created_at, })), }); }