diff --git a/lib/geocoder/ip2geogeocoder.js b/lib/geocoder/ip2geogeocoder.js new file mode 100644 index 0000000..a052f1d --- /dev/null +++ b/lib/geocoder/ip2geogeocoder.js @@ -0,0 +1,109 @@ +var util = require('util'), + AbstractGeocoder = require('./abstractgeocoder'); + +/** + * Constructor + * @param httpAdapter Http Adapter + * @param options Options ({ apiKey: 'xxx' }) + */ +var Ip2geoGeocoder = function Ip2geoGeocoder(httpAdapter, options) { + this.supportIPv4 = true; + this.supportIPv6 = true; + this.supportAddress = false; + + Ip2geoGeocoder.super_.call(this, httpAdapter, options); + + if (!options || !options.apiKey || options.apiKey === 'undefined') { + throw new Error(this.constructor.name + ' needs an apiKey'); + } + + // Inject X-Api-Key header into the http adapter for authentication + if (httpAdapter.options) { + httpAdapter.options.headers = Object.assign( + {}, + httpAdapter.options.headers || {}, + { 'X-Api-Key': options.apiKey } + ); + } +}; + +util.inherits(Ip2geoGeocoder, AbstractGeocoder); + +// API endpoint +Ip2geoGeocoder.prototype._endpoint = 'https://api.ip2geo.dev/convert'; + +/** + * Geocode + * @param value Value to geocode (IP address) + * @param callback Callback method + */ +Ip2geoGeocoder.prototype._geocode = function(value, callback) { + var params = { + ip: value + }; + + this.httpAdapter.get(this._endpoint, params, function(err, result) { + if (err) { + return callback(err); + } + + if (!result || !result.success || !result.data) { + return callback(new Error('ip2geo: Invalid response or request failed')); + } + + var data = result.data; + var results = []; + + var continent = data.continent || {}; + var country = (continent && continent.country) || {}; + var city = (country && country.city) || {}; + var subdiv = (country && country.subdivision) || {}; + var timezone = (city && city.timezone) || {}; + var currency = (country && country.currency) || {}; + var flag = (country && country.flag) || {}; + var asn = data.asn || {}; + var regCountry = data.registered_country || {}; + + results.push({ + 'ip' : data.ip, + 'ipType' : data.type, + 'isEu' : data.is_eu, + 'latitude' : city.latitude, + 'longitude' : city.longitude, + 'accuracyRadius' : city.accuracy_radius, + 'geonameId' : city.geoname_id, + 'metroCode' : city.metro_code, + 'continent' : continent.name, + 'continentCode' : continent.code, + 'continentGeonameId' : continent.geoname_id, + 'country' : country.name, + 'countryCode' : country.code, + 'countryGeonameId' : country.geoname_id, + 'phoneCode' : country.phone_code, + 'capital' : country.capital, + 'tld' : country.tld, + 'flagEmoji' : flag.emoji, + 'flagEmojiUnicode' : flag.emoji_unicode, + 'flagImg' : flag.img, + 'city' : city.name, + 'state' : subdiv.name, + 'stateCode' : subdiv.code, + 'zipcode' : city.postal_code, + 'timeZone' : timezone.name, + 'timeNow' : timezone.time_now, + 'currencyName' : currency.name, + 'currencyCode' : currency.code, + 'currencySymbol' : currency.symbol, + 'asnNumber' : asn.number, + 'asnName' : asn.name, + 'registeredCountry' : regCountry.name, + 'registeredCountryCode' : regCountry.code, + 'registeredCountryGeonameId' : regCountry.geoname_id + }); + + results.raw = result; + callback(false, results); + }); +}; + +module.exports = Ip2geoGeocoder; diff --git a/lib/geocoderfactory.js b/lib/geocoderfactory.js index 3283607..2213d37 100644 --- a/lib/geocoderfactory.js +++ b/lib/geocoderfactory.js @@ -27,6 +27,7 @@ const TeleportGeocoder = require('./geocoder/teleportgeocoder.js'); const OpendataFranceGeocoder = require('./geocoder/opendatafrancegeocoder.js'); const MapBoxGeocoder = require('./geocoder/mapboxgeocoder.js'); const APlaceGeocoder = require('./geocoder/aplacegeocoder.js'); +const Ip2geoGeocoder = require('./geocoder/ip2geogeocoder.js'); /** * Geocoder Facotry @@ -152,6 +153,11 @@ const GeocoderFactory = { if (geocoderName === 'mapbox') { return new MapBoxGeocoder(adapter, extra); } + if (geocoderName === 'ip2geo') { + return new Ip2geoGeocoder(adapter, { + apiKey: extra.apiKey + }); + } if (geocoderName === 'aplace') { return new APlaceGeocoder(adapter, extra); } diff --git a/test/geocoder/ip2geogeocoder.test.js b/test/geocoder/ip2geogeocoder.test.js new file mode 100644 index 0000000..58004e4 --- /dev/null +++ b/test/geocoder/ip2geogeocoder.test.js @@ -0,0 +1,113 @@ +(function() { + var chai = require('chai'), + should = chai.should(), + expect = chai.expect, + sinon = require('sinon'); + + var Ip2geoGeocoder = require('../../lib/geocoder/ip2geogeocoder.js'); + + var mockedHttpAdapter = { + get: function() {} + }; + + describe('Ip2geoGeocoder', () => { + + describe('#constructor', () => { + test('an http adapter must be set', () => { + expect(function() {new Ip2geoGeocoder();}).to.throw(Error, 'Ip2geoGeocoder need an httpAdapter'); + }); + + test('an apiKey must be set', () => { + expect(function() {new Ip2geoGeocoder(mockedHttpAdapter);}).to.throw(Error, 'Ip2geoGeocoder needs an apiKey'); + }); + + test('Should be an instance of Ip2geoGeocoder', () => { + var geocoder = new Ip2geoGeocoder(mockedHttpAdapter, {apiKey: 'test'}); + geocoder.should.be.instanceof(Ip2geoGeocoder); + }); + }); + + describe('#geocode', () => { + test('Should not accept address', () => { + var geocoder = new Ip2geoGeocoder(mockedHttpAdapter, {apiKey: 'test'}); + expect(function() {geocoder.geocode('1 rue test');}) + .to.throw(Error, 'Ip2geoGeocoder does not support geocoding address'); + }); + + test('Should call httpAdapter get method', () => { + var mock = sinon.mock(mockedHttpAdapter); + mock.expects('get').once().returns({then: function() {}}); + + var geocoder = new Ip2geoGeocoder(mockedHttpAdapter, {apiKey: 'test'}); + geocoder.geocode('8.8.8.8'); + mock.verify(); + }); + + test('Should return a geocoded address', done => { + var mock = sinon.mock(mockedHttpAdapter); + mock.expects('get').once().callsArgWith(2, false, { + success: true, + data: { + ip: '8.8.8.8', + type: 'IPv4', + is_eu: false, + continent: { + name: 'North America', + code: 'NA', + geoname_id: 6255149, + country: { + name: 'United States', + code: 'US', + geoname_id: 6252001, + phone_code: '+1', + capital: 'Washington D.C.', + tld: '.us', + flag: { emoji: '', emoji_unicode: 'U+1F1FA U+1F1F8', img: 'https://flagcdn.com/us.svg' }, + currency: { name: 'United States Dollar', code: 'USD', symbol: '$' }, + subdivision: { name: 'California', code: 'CA' }, + city: { + name: 'Mountain View', + latitude: 37.386, + longitude: -122.0838, + postal_code: '94035', + geoname_id: 5375480, + metro_code: 807, + accuracy_radius: 1000, + timezone: { name: 'America/Los_Angeles', time_now: '2026-04-05T10:30:00-07:00' } + } + } + }, + asn: { number: 15169, name: 'Google LLC' }, + registered_country: { name: 'United States', code: 'US', geoname_id: 6252001 } + } + }); + + var geocoder = new Ip2geoGeocoder(mockedHttpAdapter, {apiKey: 'test'}); + geocoder.geocode('8.8.8.8', function(err, results) { + err.should.to.equal(false); + results[0].ip.should.to.equal('8.8.8.8'); + results[0].country.should.to.equal('United States'); + results[0].countryCode.should.to.equal('US'); + results[0].city.should.to.equal('Mountain View'); + results[0].state.should.to.equal('California'); + results[0].latitude.should.to.equal(37.386); + results[0].longitude.should.to.equal(-122.0838); + results[0].zipcode.should.to.equal('94035'); + results[0].timeZone.should.to.equal('America/Los_Angeles'); + results[0].asnNumber.should.to.equal(15169); + results[0].asnName.should.to.equal('Google LLC'); + mock.verify(); + done(); + }); + }); + }); + + describe('#reverse', () => { + test('Should throw an error', () => { + var geocoder = new Ip2geoGeocoder(mockedHttpAdapter, {apiKey: 'test'}); + expect(function() {geocoder.reverse(10.0235, -2.3662);}) + .to.throw(Error, 'Ip2geoGeocoder no support reverse geocoding'); + }); + }); + }); +})();