SPB Git

Repository Public

route.ts 3.8 KB 88 lines typescript

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
Files main
Menu

Theme