import { getSubsiteByHost } from '../../utils/subsites.js'; import { checkRateLimit, recordRequest, getCookieId, generateNonce, createNonceCookie, getClientIp, } from '../../utils/rateLimit.js'; export async function GET({ request }) { const host = request.headers.get('host'); const subsite = getSubsiteByHost(host); if (!subsite || subsite.short !== 'req') { return new Response('Not found', { status: 404 }); } // Rate limit check (5 requests per second, flood protection at 30/10s) const rateCheck = checkRateLimit(request, { limit: 5, windowMs: 1000, burstLimit: 30, burstWindowMs: 10_000, }); let cookieId = getCookieId(request); const hadCookie = !!cookieId; if (!cookieId) { cookieId = generateNonce(); } if (!rateCheck.allowed) { const errorMsg = rateCheck.isFlooding ? { error: 'Too many requests - please slow down' } : { error: 'Rate limit exceeded' }; const response = new Response(JSON.stringify(errorMsg), { status: 429, headers: { 'Content-Type': 'application/json', 'Retry-After': '1', }, }); if (!hadCookie) { response.headers.set('Set-Cookie', createNonceCookie(cookieId)); } console.log(`[search] rate limited: ip=${rateCheck.ip} flooding=${rateCheck.isFlooding}`); return response; } // Record the request for rate limiting recordRequest(request, 1000); const TMDB_API_KEY = import.meta.env.TMDB_API_KEY; if (!TMDB_API_KEY) { console.error('TMDB_API_KEY not set'); const response = new Response(JSON.stringify([]), { status: 500, headers: { 'Content-Type': 'application/json' }, }); if (!hadCookie) { response.headers.set('Set-Cookie', createNonceCookie(cookieId)); } return response; } const url = new URL(request.url); const q = url.searchParams.get('q'); if (!q || typeof q !== 'string' || !q.trim()) { const response = new Response(JSON.stringify([]), { status: 200, headers: { 'Content-Type': 'application/json' }, }); if (!hadCookie) { response.headers.set('Set-Cookie', createNonceCookie(cookieId)); } return response; } if (q.length > 100) { const response = new Response(JSON.stringify([]), { status: 200, headers: { 'Content-Type': 'application/json' }, }); if (!hadCookie) { response.headers.set('Set-Cookie', createNonceCookie(cookieId)); } return response; } try { const apiResponse = await fetch(`https://api.themoviedb.org/3/search/multi?api_key=${TMDB_API_KEY}&query=${encodeURIComponent(q)}`); if (!apiResponse.ok) { throw new Error(`TMDB API error: ${apiResponse.status}`); } const data = await apiResponse.json(); const seen = new Set(); const filtered = data.results .filter(item => { if (item.media_type !== 'movie' && item.media_type !== 'tv') return false; const key = `${item.media_type}-${item.title || item.name}-${item.release_date?.split('-')[0] || item.first_air_date?.split('-')[0] || ''}`; if (seen.has(key)) return false; seen.add(key); return true; }) .slice(0, 10) // Limit to 10 suggestions .map(item => ({ label: item.title || item.name, value: item.title || item.name, year: item.release_date?.split('-')[0] || item.first_air_date?.split('-')[0], mediaType: item.media_type, overview: item.overview, poster_path: item.poster_path, })); const response = new Response(JSON.stringify(filtered), { headers: { 'Content-Type': 'application/json' }, }); if (!hadCookie) { response.headers.set('Set-Cookie', createNonceCookie(cookieId)); } return response; } catch (error) { console.error('Error fetching suggestions:', error); const response = new Response(JSON.stringify([]), { status: 500, headers: { 'Content-Type': 'application/json' }, }); if (!hadCookie) { response.headers.set('Set-Cookie', createNonceCookie(cookieId)); } return response; } }