gpt-engineer-app[bot] 8e8214837a Unified mod display sources
Co-authored-by: felix-fx-top <253056634+felix-fx-top@users.noreply.github.com>
2026-03-31 10:43:02 +00:00

130 lines
4.4 KiB
TypeScript

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",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin",
};
const CF_BASE = "https://api.curseforge.com/v1";
const MINECRAFT_GAME_ID = 432;
// Rate limiting
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
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<string, string> = {
"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" },
});
}
});