Repository Public
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// KA ID v2 — recherches sauvegardées universelles (serveur-à-serveur).3// Le hub est LE magasin central : chaque plateforme sauvegarde et relit les4// recherches de ses membres, signées du secret SSO partagé.5// GET ?client_id&ka_id&ts&sig[&scope=app|all] (défaut : app)6// POST { client_id, ka_id, ts, sig, action: "add"|"remove"|"touch"|"alert",7// search: { id?, label, query?, filters?, location?, url?,8// alert?, frequency? } }9import { NextRequest, NextResponse } from "next/server";10import { findUserByKaId } from "@/lib/db";11import { verifyClientSig } from "@/lib/sso";12import {13 savedSearchesOf, addSavedSearch, removeSavedSearch, touchSavedSearch,14 setSavedSearchAlert, ingestEvents,15} from "@/lib/kaid-data";1617function serialize(rows: ReturnType<typeof savedSearchesOf>) {18 return rows.map((s) => ({19 id: s.id, app: s.app, label: s.label, query: s.query,20 filters: s.filters ? JSON.parse(s.filters) : null,21 location: s.location, url: s.url,22 alert_enabled: !!s.alert_enabled, alert_frequency: s.alert_frequency,23 created_at: s.created_at, last_run_at: s.last_run_at,24 }));25}2627export async function GET(req: NextRequest) {28 const q = req.nextUrl.searchParams;29 const clientId = q.get("client_id") ?? "";30 const kaId = q.get("ka_id") ?? "";31 if (!verifyClientSig(clientId, kaId, q.get("ts") ?? "", q.get("sig") ?? ""))32 return NextResponse.json({ error: "signature invalide" }, { status: 401 });33 const user = findUserByKaId(kaId);34 if (!user)35 return NextResponse.json({ error: "membre introuvable" }, { status: 404 });36 const scope = q.get("scope") === "all" ? undefined : clientId;37 return NextResponse.json({ ok: true, searches: serialize(savedSearchesOf(user.id, scope)) });38}3940export async function POST(req: NextRequest) {41 const body = (await req.json().catch(() => ({}))) as Record<string, unknown>;42 const clientId = String(body.client_id ?? "").slice(0, 30);43 const kaId = String(body.ka_id ?? "").slice(0, 20);44 if (!verifyClientSig(clientId, kaId, String(body.ts ?? ""), String(body.sig ?? "")))45 return NextResponse.json({ error: "signature invalide" }, { status: 401 });46 const user = findUserByKaId(kaId);47 if (!user)48 return NextResponse.json({ error: "membre introuvable" }, { status: 404 });4950 const s = (body.search ?? {}) as Record<string, unknown>;51 const action = String(body.action ?? "add");5253 if (action === "remove" && typeof s.id === "number") {54 removeSavedSearch(user.id, s.id);55 return NextResponse.json({ ok: true, action });56 }57 if (action === "touch" && typeof s.id === "number") {58 touchSavedSearch(user.id, s.id);59 return NextResponse.json({ ok: true, action });60 }61 if (action === "alert" && typeof s.id === "number") {62 setSavedSearchAlert(user.id, s.id, !!s.alert,63 typeof s.frequency === "string" ? s.frequency : undefined);64 return NextResponse.json({ ok: true, action });65 }66 if (action === "add") {67 const label = String(s.label ?? "").trim();68 if (!label)69 return NextResponse.json({ error: "label requis" }, { status: 400 });70 const id = addSavedSearch(user.id, clientId, {71 label,72 query: typeof s.query === "string" ? s.query : undefined,73 filters: s.filters,74 location: typeof s.location === "string" ? s.location : undefined,75 url: typeof s.url === "string" ? s.url : undefined,76 alert: !!s.alert,77 frequency: typeof s.frequency === "string" ? s.frequency : undefined,78 });79 ingestEvents(user.id, clientId, [{80 type: s.alert ? "alert_create" : "saved_search",81 query: typeof s.query === "string" ? s.query : undefined,82 filters: s.filters,83 }]);84 return NextResponse.json({ ok: true, action, id });85 }86 return NextResponse.json({ error: "action inconnue" }, { status: 400 });87}88