Added auto search keywords
Co-authored-by: felix-fx-top <253056634+felix-fx-top@users.noreply.github.com>
This commit is contained in:
parent
c125893fdf
commit
bb9422d1f0
@ -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" },
|
||||
});
|
||||
|
||||
77
supabase/functions/translate-search/index.ts
Normal file
77
supabase/functions/translate-search/index.ts
Normal 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" },
|
||||
});
|
||||
}
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user