Tudor Stanciu dca221384c chore: bump version to 1.0.1 for backend and frontend
fix: update import statements to remove file extensions for consistency
fix: improve path handling in pathCombine utility function
fix: ensure trailing slash in Vite base path configuration
refactor: update build script to use tsup for backend
refactor: clean up package.json dependencies and devDependencies
2025-10-05 03:55:22 +03:00

166 lines
4.5 KiB
TypeScript

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<void> {
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<SimplifiedGeoIPResponse> {
this.ensureInitialized();
const cacheKey = `simple_${ip}`;
const cached = this.cache.get<SimplifiedGeoIPResponse>(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<DetailedGeoIPResponse> {
this.ensureInitialized();
const cacheKey = `detailed_${ip}`;
const cached = this.cache.get<DetailedGeoIPResponse>(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<Array<SimplifiedGeoIPResponse | { ip: string; error: string }>> {
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<boolean> {
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;