feat(geolocation): add geolocation data to emails (#1334)

This commit is contained in:
Sai Prashanth Chandramouli 2016-07-29 12:41:08 -07:00 коммит произвёл Vlad Filippov
Родитель b4969e2c5b
Коммит 8132d55725
10 изменённых файлов: 593 добавлений и 886 удалений

5
.nsprc
Просмотреть файл

@ -8,6 +8,9 @@
"https://nodesecurity.io/advisories/55",
// for hapi/call dependency
"https://nodesecurity.io/advisories/121"
"https://nodesecurity.io/advisories/121",
// for devDep in maxmind
"https://nodesecurity.io/advisories/95"
]
}

Просмотреть файл

@ -17,6 +17,20 @@ var conf = convict({
format: [ 'dev', 'test', 'stage', 'prod' ],
env: 'NODE_ENV'
},
geodb: {
dbPath: {
doc: 'Path to the maxmind database file',
default: path.resolve(__dirname, '../node_modules/fxa-geodb/db/cities-db.mmdb'),
env: 'GEODB_DBPATH',
format: String
},
enabled: {
doc: 'kill-switch for geodb',
default: true,
env: 'GEODB_ENABLED',
format: Boolean
}
},
log: {
level: {
default: 'info',

56
lib/geodb.js Normal file
Просмотреть файл

@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var config = require('../config').get('geodb')
var geodb = require('fxa-geodb')(config.dbPath)
var P = require('./promise')
var ACCURACY_MAX_KM = 200
var ACCURACY_MIN_KM = 25
/**
* Thin wrapper around geodb, to help log the accuracy
* and catch errors. On success, returns an object with
* `location` data. On failure, returns an empty object
**/
module.exports = function (log) {
return function (ip) {
// this is a kill-switch and can be used to not return location data
if (config.enabled === false) {
// if kill-switch is set, return a promise that resolves
// with an empty object
return new P.resolve({})
}
return geodb(ip)
.then(function (location) {
var logEventPrefix = 'fxa.location.accuracy.'
var logEvent = 'no_accuracy_data'
var accuracy = location.accuracy
if (accuracy) {
if (accuracy > ACCURACY_MAX_KM) {
logEvent = 'unknown'
} else if (accuracy > ACCURACY_MIN_KM && accuracy <= ACCURACY_MAX_KM) {
logEvent = 'uncertain'
} else if (accuracy <= ACCURACY_MIN_KM) {
logEvent = 'confident'
}
}
log.info({op: 'geodb.accuracy', 'accuracy': accuracy})
log.info({op: 'geodb.accuracy_confidence', 'accuracy_confidence': logEventPrefix + logEvent})
return {
location: {
city: location.city,
country: location.country
},
timeZone: location.timeZone
}
}).catch(function (err) {
log.error({ op: 'geodb.1', err: err.message})
// return an empty object, so that we can still send out
// emails without the location data
return {}
})
}
}

Просмотреть файл

@ -37,9 +37,12 @@ module.exports = function (config, log) {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
code: code.toString('hex'),
email: account.email,
ip: opts.ip,
location: opts.location,
redirectTo: opts.redirectTo,
resume: opts.resume,
service: opts.service,
timeZone: opts.timeZone,
uaBrowser: opts.uaBrowser,
uaBrowserVersion: opts.uaBrowserVersion,
uaOS: opts.uaOS,
@ -80,13 +83,15 @@ module.exports = function (config, log) {
mailer.sendNewDeviceLoginNotification = function (email, opts) {
return P.resolve(mailer.newDeviceLoginEmail(
{
email: email,
acceptLanguage: opts.acceptLanguage || defaultLanguage,
email: email,
ip: opts.ip,
location: opts.location,
timeZone: opts.timeZone,
uaBrowser: opts.uaBrowser,
uaBrowserVersion: opts.uaBrowserVersion,
uaOS: opts.uaOS,
uaOSVersion: opts.uaOSVersion,
timestamp: opts.timestamp
uaOSVersion: opts.uaOSVersion
}
))
}

Просмотреть файл

@ -44,6 +44,7 @@ module.exports = function (
var schema = fs.readFileSync(schemaPath)
var validatePushPayload = ajv.compile(schema)
var verificationReminder = require('../verification-reminders')(log, db)
var getGeoData = require('../geodb')(log)
var routes = [
{
@ -343,6 +344,7 @@ module.exports = function (
var resume = request.payload.resume
var tokenVerificationId = crypto.randomBytes(16)
var emailRecord, sessions, sessionToken, keyFetchToken, doSigninConfirmation
var ip = request.app.clientAddress
metricsContext.validate(request)
@ -494,15 +496,20 @@ module.exports = function (
// not performing a sign-in confirmation.
var shouldSendNewDeviceLoginEmail = config.newLoginNotificationEnabled && requestHelper.wantsKeys(request) && !doSigninConfirmation
if (shouldSendNewDeviceLoginEmail) {
// The response doesn't have to wait for this,
// so we don't return the promise.
mailer.sendNewDeviceLoginNotification(
emailRecord.email,
userAgent.call({
acceptLanguage: request.app.acceptLanguage,
timestamp: Date.now()
}, request.headers['user-agent'])
)
return getGeoData(ip)
.then(
function (geoData) {
mailer.sendNewDeviceLoginNotification(
emailRecord.email,
userAgent.call({
acceptLanguage: request.app.acceptLanguage,
ip: ip,
location: geoData.location,
timeZone: geoData.timeZone
}, request.headers['user-agent'])
)
}
)
}
}
@ -512,17 +519,24 @@ module.exports = function (
// the tokens are created verified.
var shouldSendVerifyLoginEmail = requestHelper.wantsKeys(request) && emailRecord.emailVerified && doSigninConfirmation
if (shouldSendVerifyLoginEmail) {
mailer.sendVerifyLoginEmail(
emailRecord,
tokenVerificationId,
userAgent.call({
acceptLanguage: request.app.acceptLanguage,
timestamp: Date.now(),
service: service,
redirectTo: redirectTo,
resume: resume
}, request.headers['user-agent'])
)
return getGeoData(ip)
.then(
function (geoData) {
mailer.sendVerifyLoginEmail(
emailRecord,
tokenVerificationId,
userAgent.call({
acceptLanguage: request.app.acceptLanguage,
ip: ip,
location: geoData.location,
redirectTo: redirectTo,
resume: resume,
service: service,
timeZone: geoData.timeZone
}, request.headers['user-agent'])
)
}
)
}
}

1259
npm-shrinkwrap.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -37,6 +37,7 @@
"email-addresses": "2.0.2",
"envc": "2.4.0",
"fxa-auth-mailer": "git+https://github.com/mozilla/fxa-auth-mailer.git#master",
"fxa-geodb": "0.0.6",
"fxa-jwtool": "0.7.1",
"hapi": "8.8.1",
"hapi-auth-hawk": "3.0.1",

Просмотреть файл

@ -798,6 +798,9 @@ test('/account/login', function (t) {
t.equal(args[2], mockRequest.payload.metricsContext, 'third argument was metrics context')
t.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 1, 'mailer.sendNewDeviceLoginNotification was called')
t.equal(mockMailer.sendNewDeviceLoginNotification.getCall(0).args[1].location.city, 'Mountain View')
t.equal(mockMailer.sendNewDeviceLoginNotification.getCall(0).args[1].location.country, 'United States')
t.equal(mockMailer.sendNewDeviceLoginNotification.getCall(0).args[1].timeZone, 'America/Los_Angeles')
t.equal(mockMailer.sendVerifyLoginEmail.callCount, 0, 'mailer.sendVerifyLoginEmail was not called')
t.notOk(response.verificationMethod, 'verificationMethod doesn\'t exist')
t.notOk(response.verificationReason, 'verificationReason doesn\'t exist')
@ -807,7 +810,7 @@ test('/account/login', function (t) {
})
t.test('sign-in confirmation enabled', function (t) {
t.plan(10)
t.plan(11)
config.signinConfirmation = {
enabled: true,
supportedClients: [ 'fx_desktop_v3' ],
@ -827,6 +830,17 @@ test('/account/login', function (t) {
})
})
t.test('location data is present in sign-in confirmation email', function (t) {
return runTest(route, mockRequest, function (response) {
t.equal(mockMailer.sendVerifyLoginEmail.callCount, 1, 'mailer.sendVerifyLoginEmail was called')
t.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].location.city, 'Mountain View')
t.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].location.country, 'United States')
t.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].timeZone, 'America/Los_Angeles')
}).then(function () {
mockMailer.sendVerifyLoginEmail.reset()
})
})
t.test('on for sample', function (t) {
// Force uid to '01...'
uid.fill(0, 0, 1)

60
test/local/geodb.js Normal file
Просмотреть файл

@ -0,0 +1,60 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var tap = require('tap')
var proxyquire = require('proxyquire')
var test = tap.test
var mockLog = require('../mocks').mockLog
test(
'returns location data when enabled',
function (t) {
var moduleMocks = {
'../config': {
'get': function (item) {
if (item === 'geodb') {
return {
enabled: true
}
}
}
}
}
var thisMockLog = mockLog({})
var getGeoData = proxyquire('../../lib/geodb', moduleMocks)(thisMockLog)
getGeoData('8.8.8.8')
.then(function (geoData) {
t.equal(geoData.location.city, 'Mountain View')
t.equal(geoData.location.country, 'United States')
t.equal(geoData.timeZone, 'America/Los_Angeles')
t.end()
})
}
)
test(
'returns empty object data when disabled',
function (t) {
var moduleMocks = {
'../config': {
'get': function (item) {
if (item === 'geodb') {
return {
enabled: false
}
}
}
}
}
var thisMockLog = mockLog({})
var getGeoData = proxyquire('../../lib/geodb', moduleMocks)(thisMockLog)
getGeoData('8.8.8.8')
.then(function (geoData) {
t.deepEqual(geoData, {})
t.end()
})
}
)

Просмотреть файл

@ -175,7 +175,8 @@ function spyLog (methods) {
function mockRequest (data) {
return {
app: {
acceptLangage: 'en-US'
acceptLangage: 'en-US',
clientAddress: '8.8.8.8'
},
auth: {
credentials: data.credentials