From bb9422d1f032349b7852fdaeb39b698abbc1470a Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:52:38 +0000 Subject: [PATCH] Added auto search keywords Co-authored-by: felix-fx-top <253056634+felix-fx-top@users.noreply.github.com> --- supabase/functions/fetch-mods/index.ts | 73 +++++++++++++++---- supabase/functions/translate-search/index.ts | 77 ++++++++++++++++++++ 2 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 supabase/functions/translate-search/index.ts diff --git a/supabase/functions/fetch-mods/index.ts b/supabase/functions/fetch-mods/index.ts index b9e12d0..6b8f146 100644 --- a/supabase/functions/fetch-mods/index.ts +++ b/supabase/functions/fetch-mods/index.ts @@ -2,20 +2,59 @@ import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; const corsHeaders = { "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", + "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type, x-supabase-client-platform, x-supabase-client-platform-version, x-supabase-client-runtime, x-supabase-client-runtime-version", }; const MODRINTH_BASE = "https://api.modrinth.com/v2"; +// Rate limiting: track requests per IP +const rateLimitMap = new Map(); +const RATE_LIMIT = 30; // requests per window +const RATE_WINDOW = 60_000; // 1 minute + +function checkRateLimit(ip: string): boolean { + const now = Date.now(); + const entry = rateLimitMap.get(ip); + if (!entry || now > entry.resetAt) { + rateLimitMap.set(ip, { count: 1, resetAt: now + RATE_WINDOW }); + return true; + } + entry.count++; + return entry.count <= RATE_LIMIT; +} + +// Input sanitization +function sanitizeString(str: string | null, maxLen = 100): string { + if (!str) return ""; + return str.slice(0, maxLen).replace(/[<>{}]/g, "").trim(); +} + serve(async (req) => { if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } + // Only allow GET + if (req.method !== "GET") { + return new Response(JSON.stringify({ error: "Method not allowed" }), { + status: 405, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + // Rate limit check + const clientIp = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown"; + if (!checkRateLimit(clientIp)) { + return new Response(JSON.stringify({ error: "Too many requests. Please try again later." }), { + status: 429, + headers: { ...corsHeaders, "Content-Type": "application/json", "Retry-After": "60" }, + }); + } + try { const apiKey = Deno.env.get("MODRINTH_API_KEY"); const url = new URL(req.url); - const action = url.searchParams.get("action"); + const action = sanitizeString(url.searchParams.get("action"), 20); const headers: Record = { "User-Agent": "FXCraft/1.0", @@ -28,15 +67,21 @@ serve(async (req) => { switch (action) { case "user_projects": { - const username = url.searchParams.get("username") || "fxfelixzero"; + const username = sanitizeString(url.searchParams.get("username"), 30) || "fxfelixzero"; + if (!/^[a-zA-Z0-9_-]+$/.test(username)) { + return new Response(JSON.stringify({ error: "Invalid username" }), { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } modrinthUrl = `${MODRINTH_BASE}/user/${username}/projects`; break; } case "search": { - const query = url.searchParams.get("query") || ""; - const facets = url.searchParams.get("facets") || ""; - const offset = url.searchParams.get("offset") || "0"; - const limit = url.searchParams.get("limit") || "20"; + const query = sanitizeString(url.searchParams.get("query"), 200); + const facets = sanitizeString(url.searchParams.get("facets"), 500); + const offset = Math.max(0, Math.min(parseInt(url.searchParams.get("offset") || "0") || 0, 1000)); + const limit = Math.max(1, Math.min(parseInt(url.searchParams.get("limit") || "20") || 20, 100)); modrinthUrl = `${MODRINTH_BASE}/search?query=${encodeURIComponent(query)}&offset=${offset}&limit=${limit}`; if (facets) { modrinthUrl += `&facets=${encodeURIComponent(facets)}`; @@ -44,9 +89,9 @@ serve(async (req) => { break; } case "project": { - const id = url.searchParams.get("id"); - if (!id) { - return new Response(JSON.stringify({ error: "Missing project id" }), { + const id = sanitizeString(url.searchParams.get("id"), 50); + if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) { + return new Response(JSON.stringify({ error: "Invalid project id" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); @@ -55,9 +100,9 @@ serve(async (req) => { break; } case "versions": { - const id = url.searchParams.get("id"); - if (!id) { - return new Response(JSON.stringify({ error: "Missing project id" }), { + const id = sanitizeString(url.searchParams.get("id"), 50); + if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) { + return new Response(JSON.stringify({ error: "Invalid project id" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); @@ -80,7 +125,7 @@ serve(async (req) => { headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } catch (error) { - return new Response(JSON.stringify({ error: error.message }), { + return new Response(JSON.stringify({ error: "Internal server error" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); diff --git a/supabase/functions/translate-search/index.ts b/supabase/functions/translate-search/index.ts new file mode 100644 index 0000000..d7f77bc --- /dev/null +++ b/supabase/functions/translate-search/index.ts @@ -0,0 +1,77 @@ +import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type, x-supabase-client-platform, x-supabase-client-platform-version, x-supabase-client-runtime, x-supabase-client-runtime-version", +}; + +serve(async (req) => { + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + try { + const { query } = await req.json(); + + if (!query || typeof query !== "string" || query.trim().length === 0 || query.length > 200) { + return new Response(JSON.stringify({ error: "Invalid query" }), { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY"); + if (!LOVABLE_API_KEY) { + return new Response(JSON.stringify({ error: "AI not configured" }), { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + const response = await fetch("https://ai.gateway.lovable.dev/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${LOVABLE_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "google/gemini-2.5-flash-lite", + messages: [ + { + role: "system", + content: "You are a translator. Translate the user's Minecraft-related search query to English. Return ONLY the translated text, nothing else. If it's already in English, return it as-is. Keep Minecraft mod names unchanged.", + }, + { role: "user", content: query.trim() }, + ], + }), + }); + + if (!response.ok) { + if (response.status === 429) { + return new Response(JSON.stringify({ error: "Rate limited" }), { + status: 429, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + const t = await response.text(); + console.error("AI error:", response.status, t); + return new Response(JSON.stringify({ error: "Translation failed" }), { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + const data = await response.json(); + const translated = data.choices?.[0]?.message?.content?.trim() || query; + + return new Response(JSON.stringify({ translated, original: query.trim() }), { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("translate error:", error); + return new Response(JSON.stringify({ error: "Translation failed" }), { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } +});