Added auto search keywords

Co-authored-by: felix-fx-top <253056634+felix-fx-top@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-03-30 13:52:38 +00:00
parent c125893fdf
commit bb9422d1f0
2 changed files with 136 additions and 14 deletions

View File

@ -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<string, { count: number; resetAt: number }>();
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<string, string> = {
"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" },
});

View File

@ -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" },
});
}
});