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

166 lines
6.3 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 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 = sanitizeString(url.searchParams.get("action"), 20);
const headers: Record<string, string> = {
"User-Agent": "FXCraft/1.0",
};
if (apiKey) {
headers["Authorization"] = apiKey;
}
let modrinthUrl = "";
switch (action) {
case "user_projects": {
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 = 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)}`;
}
break;
}
case "project": {
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" },
});
}
modrinthUrl = `${MODRINTH_BASE}/project/${id}`;
break;
}
case "versions": {
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" },
});
}
modrinthUrl = `${MODRINTH_BASE}/project/${id}/version`;
break;
}
default:
return new Response(JSON.stringify({ error: "Invalid action" }), {
status: 400,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const response = await fetch(modrinthUrl, { headers });
let data = await response.json();
// Filter out felix-fx-i and remove modrinth links from descriptions
if (Array.isArray(data)) {
data = data.filter((mod: any) => mod.slug !== 'felix-fx-i' && mod.id !== 'felix-fx-i');
data = data.map((mod: any) => ({
...mod,
description: mod.description ? mod.description.replace(/https?:\/\/modrinth\.com\/[^\s\)\]\}]*/g, '').trim() : mod.description,
body: mod.body ? mod.body.replace(/https?:\/\/modrinth\.com\/[^\s\)\]\}]*/g, '').trim() : mod.body
}));
} else if (data && typeof data === 'object' && 'hits' in data && Array.isArray(data.hits)) {
data.hits = data.hits.filter((mod: any) => mod.slug !== 'felix-fx-i' && mod.id !== 'felix-fx-i');
data.hits = data.hits.map((mod: any) => ({
...mod,
description: mod.description ? mod.description.replace(/https?:\/\/modrinth\.com\/[^\s\)\]\}]*/g, '').trim() : mod.description
}));
} else if (data && typeof data === 'object') {
if (data.slug === 'felix-fx-i' || data.id === 'felix-fx-i') {
return new Response(JSON.stringify({ error: 'Project not found' }), {
status: 404,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
if (data.description) {
data.description = data.description.replace(/https?:\/\/modrinth\.com\/[^\s\)\]\}]*/g, '').trim();
}
if (data.body) {
data.body = data.body.replace(/https?:\/\/modrinth\.com\/[^\s\)\]\}]*/g, '').trim();
}
}
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" },
});
}
});