mirror of
https://dev.azure.com/tstanciu94/PhantomMind/_git/Bitip
synced 2025-10-13 01:52:19 +03:00
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
166 lines
4.5 KiB
TypeScript
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;
|