import { NextRequest, NextResponse } from 'next/server'; import type { ApiResponse } from '@/lib/types'; interface IpLocationData { ip: string; country_name: string; country_code: string; city: string; region: string; continent_code: string; continent_name: string; latitude: number; longitude: number; timestamp: number; } // Simple in-memory cache on the server side to reduce API calls const serverCache: Record = {}; // Cache for IPs that have repeatedly failed to resolve const failedIPsCache: Record = {}; // Cache expiration time (30 days in milliseconds) const CACHE_EXPIRATION = 30 * 24 * 60 * 60 * 1000; // 30 days // Max attempts to fetch an IP before considering it permanently failed const MAX_RETRY_ATTEMPTS = 3; // Retry timeout - how long to wait before trying a failed IP again (24 hours) const RETRY_TIMEOUT = 24 * 60 * 60 * 1000; /** * Check if an IP has failed too many times and should be skipped */ function shouldSkipIP(ip: string): boolean { if (!failedIPsCache[ip]) return false; const now = Date.now(); // Skip if max attempts reached if (failedIPsCache[ip].attempts >= MAX_RETRY_ATTEMPTS) { return true; } // Skip if last attempt was recent if (now - failedIPsCache[ip].lastAttempt < RETRY_TIMEOUT) { return true; } return false; } /** * Mark an IP as failed */ function markIPAsFailed(ip: string): void { const now = Date.now(); if (failedIPsCache[ip]) { failedIPsCache[ip] = { attempts: failedIPsCache[ip].attempts + 1, lastAttempt: now }; } else { failedIPsCache[ip] = { attempts: 1, lastAttempt: now }; } } /** * Get location data for a single IP using ipapi.co */ async function fetchIpLocation(ip: string): Promise { try { // Skip this IP if it has failed too many times if (shouldSkipIP(ip)) { console.log(`[Server] Skipping blacklisted IP: ${ip}`); return null; } // Check server cache first const now = Date.now(); if (serverCache[ip] && (now - serverCache[ip].timestamp) < CACHE_EXPIRATION) { return serverCache[ip]; } // Add delay to avoid rate limiting (100 requests per minute max) await new Promise(resolve => setTimeout(resolve, 600)); // ~100 req/min = 1 req per 600ms const response = await fetch(`https://ipapi.co/${ip}/json/`); if (!response.ok) { console.error(`Error fetching location for IP ${ip}: ${response.statusText}`); markIPAsFailed(ip); return null; } const data = await response.json(); if (data.error) { console.error(`Error fetching location for IP ${ip}: ${data.reason}`); markIPAsFailed(ip); return null; } // Reset failed status if successful if (failedIPsCache[ip]) { delete failedIPsCache[ip]; } const locationData: IpLocationData = { ip: data.ip, country_name: data.country_name || 'Unknown', country_code: data.country_code || 'UN', city: data.city || 'Unknown', region: data.region || 'Unknown', continent_code: data.continent_code || 'UN', continent_name: getContinentName(data.continent_code) || 'Unknown', latitude: data.latitude || 0, longitude: data.longitude || 0, timestamp: Date.now() }; // Cache the result serverCache[ip] = locationData; return locationData; } catch (error) { console.error(`Error fetching location for IP ${ip}:`, error); markIPAsFailed(ip); return null; } } /** * Get continent name from continent code */ function getContinentName(code?: string): string { if (!code) return 'Unknown'; const continents: Record = { 'AF': 'Africa', 'AN': 'Antarctica', 'AS': 'Asia', 'EU': 'Europe', 'NA': 'North America', 'OC': 'Oceania', 'SA': 'South America' }; return continents[code] || 'Unknown'; } /** * API route handler for batch IP location lookups */ export async function POST(request: NextRequest) { try { const { ips } = await request.json(); if (!ips || !Array.isArray(ips) || ips.length === 0) { return NextResponse.json({ success: false, error: 'Invalid or empty IP list' }, { status: 400 }); } // Limit batch size to 50 IPs to prevent abuse const ipList = ips.slice(0, 50); const results: Record = {}; // Filter out IPs that should be skipped const validIPs = ipList.filter(ip => { if (typeof ip !== 'string' || !ip.trim()) return false; if (isPrivateIP(ip)) { results[ip] = getPrivateIPData(ip); return false; } if (shouldSkipIP(ip)) { console.log(`[Server] Skipping blacklisted IP: ${ip}`); results[ip] = null; return false; } return true; }); // Process remaining IPs sequentially to respect rate limits for (const ip of validIPs) { results[ip] = await fetchIpLocation(ip); } const response: ApiResponse> = { success: true, data: results }; return NextResponse.json(response); } catch (error) { console.error('Batch IP lookup error:', error); const response: ApiResponse = { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; return NextResponse.json(response, { status: 500 }); } } /** * Check if IP is a private/local address */ function isPrivateIP(ip: string): boolean { return ( ip.startsWith('10.') || ip.startsWith('192.168.') || ip.startsWith('172.16.') || ip.startsWith('172.17.') || ip.startsWith('172.18.') || ip.startsWith('172.19.') || ip.startsWith('172.20.') || ip.startsWith('172.21.') || ip.startsWith('172.22.') || ip.startsWith('127.') || ip === 'localhost' || ip === '::1' ); } /** * Generate location data for private IP addresses */ function getPrivateIPData(ip: string): IpLocationData { return { ip, country_name: 'Local Network', country_code: 'LO', city: 'Local', region: 'Local', continent_code: 'LO', continent_name: 'Local', latitude: 0, longitude: 0, timestamp: Date.now() }; }