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%
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