import { Reader, ReaderModel, City } from '@maxmind/geoip2-node'; import path from 'path'; import fs from 'fs'; import NodeCache from 'node-cache'; import { GeoIPLocation, SimplifiedGeoIPResponse, DetailedGeoIPResponse, } from '../types/index'; import config from './config'; import logger from './logger'; class GeoIPService { private cityReader?: ReaderModel; private cache: NodeCache; private dbPath: string; private isInitialized: boolean = false; constructor() { this.cache = new NodeCache({ stdTTL: 300, checkperiod: 60 }); // 5 minutes cache this.dbPath = config.maxmindDbPath; this.initializeReader(); } private async initializeReader(): Promise { try { const cityDbPath = path.join(this.dbPath, 'GeoLite2-City.mmdb'); if (!fs.existsSync(cityDbPath)) { throw new Error(`GeoIP database not found at ${cityDbPath}`); } this.cityReader = await Reader.open(cityDbPath); this.isInitialized = true; logger.info('GeoIP database initialized successfully', { dbPath: cityDbPath, }); } catch (error) { logger.error('Failed to initialize GeoIP database', error as Error, { dbPath: this.dbPath, }); throw error; } } private ensureInitialized(): void { if (!this.isInitialized || !this.cityReader) { throw new Error('GeoIP service not initialized'); } } async lookupSimple(ip: string): Promise { this.ensureInitialized(); const cacheKey = `simple_${ip}`; const cached = this.cache.get(cacheKey); if (cached) { return cached; } try { const response: City = this.cityReader!.city(ip); const result: SimplifiedGeoIPResponse = { ip, country: response.country?.names?.en || 'Unknown', country_code: response.country?.isoCode || 'XX', region: response.subdivisions?.[0]?.names?.en || 'Unknown', city: response.city?.names?.en || 'Unknown', latitude: response.location?.latitude || null, longitude: response.location?.longitude || null, timezone: response.location?.timeZone || null, postal_code: response.postal?.code || null, }; this.cache.set(cacheKey, result); logger.debug('GeoIP lookup completed', { ip, result }); return result; } catch (error) { logger.error('GeoIP lookup failed', error as Error, { ip }); throw error; } } async lookupDetailed(ip: string): Promise { this.ensureInitialized(); const cacheKey = `detailed_${ip}`; const cached = this.cache.get(cacheKey); if (cached) { return cached; } try { const response: City = this.cityReader!.city(ip); const result: DetailedGeoIPResponse = { ip, location: response as GeoIPLocation, }; this.cache.set(cacheKey, result); logger.debug('Detailed GeoIP lookup completed', { ip }); return result; } catch (error) { logger.error('Detailed GeoIP lookup failed', error as Error, { ip }); throw error; } } async lookupBatch( ips: string[] ): Promise> { const results = await Promise.allSettled( ips.map(async ip => this.lookupSimple(ip)) ); return results.map((result, index) => { if (result.status === 'fulfilled') { return result.value; } else { return { ip: ips[index] || 'unknown', error: result.reason?.message || 'Lookup failed', }; } }); } isValidIP(ip: string): boolean { const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; return ipv4Regex.test(ip) || ipv6Regex.test(ip); } isPrivateIP(ip: string): boolean { const privateRanges = [ /^10\./, /^172\.(1[6-9]|2[0-9]|3[0-1])\./, /^192\.168\./, /^127\./, /^169\.254\./, /^::1$/, /^fc00:/, /^fe80:/, ]; return privateRanges.some(range => range.test(ip)); } async healthCheck(): Promise { try { await this.lookupSimple('8.8.8.8'); // Test with Google DNS return true; } catch (error) { logger.error('GeoIP service health check failed', error as Error); return false; } } } export const geoIPService = new GeoIPService(); export default geoIPService;