diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 1fd45da..08ff994 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -11,7 +11,10 @@ const Index = () => { const { data: userMods, isLoading: loadingUser } = useQuery({ queryKey: ["user-projects"], - queryFn: () => getUserProjects(), + queryFn: async () => { + const projects = await getUserProjects(); + return (projects || []).filter((p: any) => p.slug !== "felix-fx-i" && p.id !== "felix-fx-i"); + }, }); const { data: discoverData, isLoading: loadingDiscover } = useQuery({ diff --git a/supabase/functions/fetch-curseforge/index.ts b/supabase/functions/fetch-curseforge/index.ts new file mode 100644 index 0000000..a1e65d1 --- /dev/null +++ b/supabase/functions/fetch-curseforge/index.ts @@ -0,0 +1,126 @@ +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", +}; + +const CF_BASE = "https://api.curseforge.com/v1"; +const MINECRAFT_GAME_ID = 432; + +// Rate limiting +const rateLimitMap = new Map(); +const RATE_LIMIT = 30; +const RATE_WINDOW = 60_000; + +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; +} + +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 }); + } + + if (req.method !== "GET") { + return new Response(JSON.stringify({ error: "Method not allowed" }), { + status: 405, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + const clientIp = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown"; + if (!checkRateLimit(clientIp)) { + return new Response(JSON.stringify({ error: "Too many requests" }), { + status: 429, + headers: { ...corsHeaders, "Content-Type": "application/json", "Retry-After": "60" }, + }); + } + + try { + const apiKey = Deno.env.get("CURSEFORGE_API_KEY"); + if (!apiKey) { + return new Response(JSON.stringify({ error: "CurseForge API key not configured" }), { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + const url = new URL(req.url); + const action = sanitizeString(url.searchParams.get("action"), 20); + + const headers: Record = { + "Accept": "application/json", + "x-api-key": apiKey, + }; + + let cfUrl = ""; + + switch (action) { + case "search": { + const query = sanitizeString(url.searchParams.get("query"), 200); + 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, 50)); + const classId = sanitizeString(url.searchParams.get("classId"), 10); + + cfUrl = `${CF_BASE}/mods/search?gameId=${MINECRAFT_GAME_ID}&searchFilter=${encodeURIComponent(query)}&index=${offset}&pageSize=${limit}&sortField=2&sortOrder=desc`; + if (classId) { + cfUrl += `&classId=${classId}`; + } + break; + } + case "mod": { + const id = sanitizeString(url.searchParams.get("id"), 20); + if (!id || !/^\d+$/.test(id)) { + return new Response(JSON.stringify({ error: "Invalid mod id" }), { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + cfUrl = `${CF_BASE}/mods/${id}`; + break; + } + case "files": { + const id = sanitizeString(url.searchParams.get("id"), 20); + if (!id || !/^\d+$/.test(id)) { + return new Response(JSON.stringify({ error: "Invalid mod id" }), { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + cfUrl = `${CF_BASE}/mods/${id}/files?pageSize=10`; + break; + } + default: + return new Response(JSON.stringify({ error: "Invalid action" }), { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + const response = await fetch(cfUrl, { headers }); + const data = await response.json(); + + return new Response(JSON.stringify(data), { + status: response.status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } catch (error) { + return new Response(JSON.stringify({ error: "Internal server error" }), { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } +});