geo main
This commit is contained in:
248
app/api/geo/batch/route.ts
Normal file
248
app/api/geo/batch/route.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
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()
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user