SPB Git

spb/groupe-ka Public

Groupe KA — site du holding + KA ID (compte unique & SSO des 7 plateformes). Next.js 16, SQLite, Google & Apple login.

TypeScript 85.5% HTML 8.9% CSS 5.5%
2.9 KB · 79 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// Favoris unifiés « Mon univers Ka » — API serveur-à-serveur.3// Le hub est LE magasin central : les plateformes poussent (POST) et4// lisent (GET) les favoris de leurs membres, signés du secret SSO partagé.5//   POST { client_id, ka_id, ts, sig, action: "add"|"remove",6//          item: { item_id, title, subtitle?, price_label?, image_url?,7//                  url?, meta? } }8//   GET  ?client_id&ka_id&ts&sig[&scope=app|all]   (défaut: app)9import { NextRequest, NextResponse } from "next/server";10import {11  findUserByKaId,12  upsertFavorite,13  removeFavorite,14  favoritesOf,15} from "@/lib/db";16import { verifyClientSig } from "@/lib/sso";1718const clip = (v: unknown, max: number): string =>19  typeof v === "string" ? v.trim().slice(0, max) : "";2021export async function POST(req: NextRequest) {22  const body = (await req.json().catch(() => ({}))) as Record<string, unknown>;23  const clientId = clip(body.client_id, 30);24  const kaId = clip(body.ka_id, 20);25  if (!verifyClientSig(clientId, kaId, String(body.ts ?? ""), String(body.sig ?? "")))26    return NextResponse.json({ error: "signature invalide" }, { status: 401 });27  const user = findUserByKaId(kaId);28  if (!user)29    return NextResponse.json({ error: "membre introuvable" }, { status: 404 });3031  const item = (body.item ?? {}) as Record<string, unknown>;32  const itemId = clip(item.item_id, 200);33  if (!itemId)34    return NextResponse.json({ error: "item_id requis" }, { status: 400 });3536  if (body.action === "remove") {37    removeFavorite(user.id, clientId, itemId);38    return NextResponse.json({ ok: true, action: "remove" });39  }40  const title = clip(item.title, 200);41  if (!title)42    return NextResponse.json({ error: "title requis" }, { status: 400 });43  upsertFavorite(user.id, clientId, {44    item_id: itemId,45    title,46    subtitle: clip(item.subtitle, 200) || undefined,47    price_label: clip(item.price_label, 60) || undefined,48    image_url: clip(item.image_url, 500) || undefined,49    url: clip(item.url, 500) || undefined,50    meta: item.meta,51  });52  return NextResponse.json({ ok: true, action: "add" });53}5455export async function GET(req: NextRequest) {56  const q = req.nextUrl.searchParams;57  const clientId = q.get("client_id") ?? "";58  const kaId = q.get("ka_id") ?? "";59  if (!verifyClientSig(clientId, kaId, q.get("ts") ?? "", q.get("sig") ?? ""))60    return NextResponse.json({ error: "signature invalide" }, { status: 401 });61  const user = findUserByKaId(kaId);62  if (!user)63    return NextResponse.json({ error: "membre introuvable" }, { status: 404 });64  const scope = q.get("scope") === "all" ? undefined : clientId;65  const rows = favoritesOf(user.id, scope);66  return NextResponse.json({67    favorites: rows.map((r) => ({68      app: r.app,69      item_id: r.item_id,70      title: r.title,71      subtitle: r.subtitle,72      price_label: r.price_label,73      image_url: r.image_url,74      url: r.url,75      created_at: r.created_at,76    })),77  });78}79