devsite/app/api/proxy/route.js
2026-04-23 12:21:56 +02:00

64 lines
1.9 KiB
JavaScript

/**
* Proxy Next.js vers l'API Python GrasBot.
*
* Rôle : éviter l'exposition directe du domaine `llmapi.fernandgrascalvet.com`
* depuis le navigateur (CORS, rate limiting applicatif, logging Next côté server).
*
* v3.1 (2026-04-23) — relais des IDs d'observabilité Langfuse :
* - Les paramètres `session_id` et `user_id` passés par le front (voir
* `app/utils/grasbotIds.js`) sont propagés tels quels vers l'API Python
* qui les injecte dans la trace Langfuse.
* - Whitelist stricte des query params relayés (q, session_id, user_id).
* Toute autre clé est ignorée → pas de risque de SSRF via query injection.
*/
const UPSTREAM_BASE = "https://llmapi.fernandgrascalvet.com";
const ALLOWED_PARAMS = new Set(["q", "session_id", "user_id"]);
export async function GET(req) {
const { searchParams } = new URL(req.url);
const question = searchParams.get("q");
if (!question) {
return new Response(JSON.stringify({ error: "Question manquante" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
// Construction des params upstream : whitelist only.
const upstream = new URLSearchParams();
for (const [key, value] of searchParams.entries()) {
if (ALLOWED_PARAMS.has(key) && value) {
upstream.set(key, value);
}
}
const apiUrl = `${UPSTREAM_BASE}/ask?${upstream.toString()}`;
try {
const response = await fetch(apiUrl, {
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json",
},
});
} catch (error) {
return new Response(
JSON.stringify({ error: "Erreur de communication avec l'API" }),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
);
}
}