248 lines
6.3 KiB
TypeScript
248 lines
6.3 KiB
TypeScript
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<string, IpLocationData> = {};
|
|
|
|
// Cache for IPs that have repeatedly failed to resolve
|
|
const failedIPsCache: Record<string, { attempts: number, lastAttempt: number }> = {};
|
|
|
|
// 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<IpLocationData | null> {
|
|
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<string, string> = {
|
|
'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<string, IpLocationData | null> = {};
|
|
|
|
// 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<Record<string, IpLocationData | null>> = {
|
|
success: true,
|
|
data: results
|
|
};
|
|
|
|
return NextResponse.json(response);
|
|
} catch (error) {
|
|
console.error('Batch IP lookup error:', error);
|
|
|
|
const response: ApiResponse<null> = {
|
|
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()
|
|
};
|
|
}
|