feat(geolocation): add geolocation data to emails (#1334)
This commit is contained in:
Родитель
b4969e2c5b
Коммит
8132d55725
5
.nsprc
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',
|
||||
|
|
|
@ -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'])
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче