Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
feat(push): send push notification on account deletion
Browse files Browse the repository at this point in the history
  • Loading branch information
eoger committed May 18, 2017
1 parent 7ba4f67 commit 163e2f4
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 6 deletions.
35 changes: 34 additions & 1 deletion docs/pushpayloads.schema.json
Expand Up @@ -9,7 +9,8 @@
{ "$ref":"#/definitions/profileUpdated" },
{ "$ref":"#/definitions/collectionsChanged" },
{ "$ref":"#/definitions/passwordChanged" },
{ "$ref":"#/definitions/passwordReset" }
{ "$ref":"#/definitions/passwordReset" },
{ "$ref":"#/definitions/accountDestroyed" }
],
"definitions":{
"deviceConnected":{
Expand Down Expand Up @@ -169,6 +170,38 @@
}
},
"additionalProperties": false
},
"accountDestroyed":{
"required":[
"version",
"command",
"data"
],
"properties":{
"version":{
"type":"integer"
},
"command":{
"type":"string",
"enum":[
"fxaccounts:account_destroyed"
]
},
"data":{
"type":"object",
"required":[
"uid"
],
"properties":{
"uid":{
"type":"string",
"description":"The UID of the account which was destroyed"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
}
}
31 changes: 30 additions & 1 deletion lib/push.js
Expand Up @@ -17,12 +17,14 @@ var PUSH_COMMANDS = {
DEVICE_DISCONNECTED: 'fxaccounts:device_disconnected',
PROFILE_UPDATED: 'fxaccounts:profile_updated',
PASSWORD_CHANGED: 'fxaccounts:password_changed',
PASSWORD_RESET: 'fxaccounts:password_reset'
PASSWORD_RESET: 'fxaccounts:password_reset',
ACCOUNT_DESTROYED: 'fxaccounts:account_destroyed'
}

var TTL_DEVICE_DISCONNECTED = 5 * 3600 // 5 hours
var TTL_PASSWORD_CHANGED = 6 * 3600 // 6 hours
var TTL_PASSWORD_RESET = TTL_PASSWORD_CHANGED
var TTL_ACCOUNT_DESTROYED = TTL_DEVICE_DISCONNECTED

// An arbitrary, but very generous, limit on the number of active devices.
// Currently only for metrics purposes, not enforced.
Expand Down Expand Up @@ -92,6 +94,14 @@ var reasonToEvents = {
failed: 'push.devices_notify.failed',
noCallback: 'push.devices_notify.no_push_callback',
noKeys: 'push.devices_notify.data_but_no_keys'
},
accountDestroyed: {
send: 'push.account_destroyed.send',
success: 'push.account_destroyed.success',
resetSettings: 'push.account_destroyed.reset_settings',
failed: 'push.account_destroyed.failed',
noCallback: 'push.account_destroyed.no_push_callback',
noKeys: 'push.account_destroyed.data_but_no_keys'
}
}

Expand Down Expand Up @@ -258,6 +268,25 @@ module.exports = function (log, db, config) {
return this.sendPush(uid, devices, 'passwordReset', options)
},

/**
* Notifies a set of devices that the account no longer exists
*
* @param {Buffer} uid
* @param {Device[]} devices
* @promise
*/
notifyAccountDestroyed: function notifyAccountDestroyed(uid, devices) {
var data = Buffer.from(JSON.stringify({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.ACCOUNT_DESTROYED,
data: {
uid: uid.toString('hex')
}
}))
var options = { data: data, TTL: TTL_ACCOUNT_DESTROYED }
return this.sendPush(uid, devices, 'accountDestroyed', options)
},

/**
* Send a push notification with or without data to all the devices in the account (except the ones in the excludedDeviceIds)
*
Expand Down
16 changes: 13 additions & 3 deletions lib/routes/account.js
Expand Up @@ -2589,35 +2589,45 @@ module.exports = (
var form = request.payload
var authPW = Buffer(form.authPW, 'hex')
var uid
var devicesToNotify
customs.check(
request,
form.email,
'accountDestroy')
.then(db.emailRecord.bind(db, form.email))
.then(
function (emailRecord) {
uid = emailRecord.uid.toString('hex')
uid = emailRecord.uid

return checkPassword(emailRecord, authPW, request.app.clientAddress)
.then(
function (match) {
if (! match) {
throw error.incorrectPassword(emailRecord.email, form.email)
}
// We fetch the devices to notify before deleteAccount()
// because obviously we can't retrieve the devices list after!
return db.devices(uid)
}
)
.then(
function (devices) {
devicesToNotify = devices
return db.deleteAccount(emailRecord)
}
)
.then(
function () {
push.notifyAccountDestroyed(uid, devicesToNotify).catch(function () {})
return log.notifyAttachedServices('delete', request, {
uid: uid + '@' + config.domain
uid: uid.toString('hex') + '@' + config.domain
})
}
)
.then(
function () {
return request.emitMetricsEvent('account.deleted', {
uid: uid
uid: uid.toString('hex')
})
}
)
Expand Down
39 changes: 39 additions & 0 deletions test/local/push.js
Expand Up @@ -579,6 +579,45 @@ describe('push', () => {
}
)

it(
'notifyAccountDestroyed calls sendPush',
() => {
var mocks = {
'web-push': {
sendNotification: function (sub, payload, options) {
return P.resolve()
}
}
}
var push = proxyquire(pushModulePath, mocks)(mockLog(), mockDbEmpty, mockConfig)
sinon.spy(push, 'sendPush')
var expectedData = {
version: 1,
command: 'fxaccounts:account_destroyed',
data: {
uid: mockUid.toString('hex')
}
}
return push.notifyAccountDestroyed(mockUid, mockDevices).catch(function (err) {
assert.fail('must not throw')
throw err
})
.then(function() {
assert.ok(push.sendPush.calledOnce, 'sendPush was called')
assert.equal(push.sendPush.getCall(0).args[0], mockUid)
assert.equal(push.sendPush.getCall(0).args[1], mockDevices)
assert.equal(push.sendPush.getCall(0).args[2], 'accountDestroyed')
var options = push.sendPush.getCall(0).args[3]
var payload = JSON.parse(options.data.toString('utf8'))
assert.deepEqual(payload, expectedData)
var schemaPath = path.resolve(__dirname, PUSH_PAYLOADS_SCHEMA_PATH)
var schema = JSON.parse(fs.readFileSync(schemaPath))
assert.ok(ajv.validate(schema, payload))
push.sendPush.restore()
})
}
)

it(
'sendPush includes VAPID identification if it is configured',
() => {
Expand Down
7 changes: 6 additions & 1 deletion test/local/routes/account.js
Expand Up @@ -1144,6 +1144,7 @@ describe('/account/destroy', function () {
authPW: new Array(65).join('f')
}
})
const mockPush = mocks.mockPush()
var accountRoutes = makeRoutes({
checkPassword: function () {
return P.resolve(true)
Expand All @@ -1152,7 +1153,8 @@ describe('/account/destroy', function () {
domain: 'wibble'
},
db: mockDB,
log: mockLog
log: mockLog,
push: mockPush
})
var route = getRoute(accountRoutes, '/account/destroy')

Expand All @@ -1169,6 +1171,9 @@ describe('/account/destroy', function () {
assert.equal(args[0].email, email, 'db.deleteAccount was passed email record')
assert.deepEqual(args[0].uid, uid, 'email record had correct uid')

assert.equal(mockPush.notifyAccountDestroyed.callCount, 1)
assert.equal(mockPush.notifyAccountDestroyed.firstCall.args[0], uid)

assert.equal(mockLog.notifyAttachedServices.callCount, 1, 'log.notifyAttachedServices was called once')
args = mockLog.notifyAttachedServices.args[0]
assert.equal(args.length, 3, 'log.notifyAttachedServices was passed three arguments')
Expand Down
1 change: 1 addition & 0 deletions test/mocks.js
Expand Up @@ -105,6 +105,7 @@ const PUSH_METHOD_NAMES = [
'notifyDeviceDisconnected',
'notifyPasswordChanged',
'notifyPasswordReset',
'notifyAccountDestroyed',
'notifyUpdate',
'pushToAllDevices',
'pushToDevices'
Expand Down

0 comments on commit 163e2f4

Please sign in to comment.