diff --git a/config/routes.js b/config/routes.js index d4ed474d..1ad874c8 100644 --- a/config/routes.js +++ b/config/routes.js @@ -63,6 +63,9 @@ module.exports = function(app) { app.post(apiPrefix + '/email/register', emailPlugin.oldSave); app.get(apiPrefix + '/email/retrieve/:email', emailPlugin.oldRetrieve); + + app.post(apiPrefix + '/email/delete/profile', emailPlugin.eraseProfile); + app.post(apiPrefix + '/email/delete/item/:key', emailPlugin.erase); } // Address routes diff --git a/plugins/emailstore.js b/plugins/emailstore.js index 5ca06410..154ee3a9 100644 --- a/plugins/emailstore.js +++ b/plugins/emailstore.js @@ -302,16 +302,46 @@ }); }; - emailPlugin.retrieveDataByEmailAndPassphrase = function(email, key, passphrase, callback) { - emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { + emailPlugin.deleteByEmailAndKey = function deleteByEmailAndKey(email, key, callback) { + emailPlugin.db.del(valueKey(email, key), function(error) { + if (error) { + if (error.notFound) { + return callback(emailPlugin.errors.NOT_FOUND); + } else { + logger.error(error); + return callback(emailPlugin.errors.INTERNAL_ERROR); + } + } + return callback(); + }); + }; + + emailPlugin.deleteWholeProfile = function deleteWholeProfile(email, callback) { + var dismissNotFound = function(callback) { + return function(error, result) { + if (error && error.notFound) { + return callback(); + } + return callback(error, result); + }; + }; + async.parallel([ + + function(callback) { + emailPlugin.db.del(emailToPassphrase(email), dismissNotFound(callback)); + }, + function(callback) { + emailPlugin.db.del(pendingKey(email), dismissNotFound(callback)); + }, + function(callback) { + emailPlugin.db.del(validatedKey(email), dismissNotFound(callback)); + } + ], function(err) { if (err) { - return callback(err); - } - if (matches) { - return emailPlugin.retrieveByEmailAndKey(email, key, callback); - } else { - return callback(emailPlugin.errors.INVALID_CODE); + logger.error(err); + return callback(emailPlugin.errors.INTERNAL_ERROR); } + return callback(); }); }; @@ -424,7 +454,6 @@ }); }; - emailPlugin.getCredentialsFromRequest = function(request) { var auth = request.header('authorization'); if (!auth) { @@ -444,52 +473,128 @@ }; }; - - emailPlugin.addValidationHeader = function(response, email, callback) { - emailPlugin.db.get(validatedKey(email), function(err, value) { - if (err && !err.notFound) + if (err && !err.notFound) { return callback(err); + } - if (value) + if (value) { return callback(); + } response.set('X-Email-Needs-Validation', 'true'); - return callback(null, value || false); + return callback(null, value); }); }; + emailPlugin.authorizeRequest = function(request, withKey, callback) { + var credentialsResult = emailPlugin.getCredentialsFromRequest(request); + if (_.contains(emailPlugin.errors, credentialsResult)) { + return callback(credentialsResult); + } + + var email = credentialsResult.email; + var passphrase = credentialsResult.passphrase; + var key; + if (withKey) { + key = request.param('key'); + } + + if (!passphrase || !email || (withKey && !key)) { + return callback(emailPlugin.errors.MISSING_PARAMETER); + } + + emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { + if (err) { + return callback(err); + } + + if (!matches) { + return callback(emailPlugin.errors.INVALID_CODE); + } + + return callback(null, email, key); + }); + }; + + emailPlugin.authorizeRequestWithoutKey = function(request, callback) { + emailPlugin.authorizeRequest(request, false, callback); + }; + + emailPlugin.authorizeRequestWithKey = function(request, callback) { + emailPlugin.authorizeRequest(request, true, callback); + }; + /** * Retrieve a record from the database */ emailPlugin.retrieve = function(request, response) { - var credentialsResult = emailPlugin.getCredentialsFromRequest(request); - if (_.contains(emailPlugin.errors, credentialsResult)) { - return emailPlugin.returnError(credentialsResult); - - } - var email = credentialsResult.email; - var passphrase = credentialsResult.passphrase; - - var key = request.param('key'); - if (!passphrase || !email || !key) { - return emailPlugin.returnError(emailPlugin.errors.MISSING_PARAMETER, response); - } - - emailPlugin.retrieveDataByEmailAndPassphrase(email, key, passphrase, function(err, value) { - if (err) + emailPlugin.authorizeRequestWithKey(request, function(err, email, key) { + if (err) { return emailPlugin.returnError(err, response); + } - emailPlugin.addValidationHeader(response, email, function(err) { - if (err) + emailPlugin.retrieveByEmailAndKey(email, key, function(err, value) { + if (err) { return emailPlugin.returnError(err, response); + } - response.send(value).end(); + emailPlugin.addValidationHeader(response, email, function(err) { + if (err) { + return emailPlugin.returnError(err, response); + } + + response.send(value).end(); + }); }); }); }; + /** + * Remove a record from the database + */ + emailPlugin.erase = function(request, response) { + emailPlugin.authorizeRequestWithKey(request, function(err, email, key) { + if (err) { + return emailPlugin.returnError(err, response); + } + emailPlugin.deleteByEmailAndKey(email, key, function(err, value) { + if (err) { + return emailPlugin.returnError(err, response); + } else { + return response.json({ + success: true + }).end(); + }; + }); + }); + }; + + /** + * Remove a whole profile from the database + * + * @TODO: This looks very similar to the method above + */ + emailPlugin.eraseProfile = function(request, response) { + emailPlugin.authorizeRequestWithoutKey(request, function(err, email) { + if (err) { + return emailPlugin.returnError(err, response); + } + + emailPlugin.deleteWholeProfile(email, function(err, value) { + if (err) { + return emailPlugin.returnError(err, response); + } else { + return response.json({ + success: true + }).end(); + }; + }); + }); + }; + + /** * Marks an email as validated * @@ -549,32 +654,28 @@ * @param {Express.Response} response */ emailPlugin.changePassphrase = function(request, response) { - var credentialsResult = emailPlugin.getCredentialsFromRequest(request); - if (_.contains(emailPlugin.errors, credentialsResult)) { - return emailPlugin.returnError(credentialsResult); - } - var email = credentialsResult.email; - var passphrase = credentialsResult.passphrase; - var queryData = ''; - request.on('data', function(data) { - queryData += data; - if (queryData.length > MAX_ALLOWED_STORAGE) { - queryData = ''; - response.writeHead(413, { - 'Content-Type': 'text/plain' - }).end(); - request.connection.destroy(); + emailPlugin.authorizeRequestWithoutKey(request, function(err, email) { + + if (err) { + return emailPlugin.returnError(err, response); } - }).on('end', function() { - var params = querystring.parse(queryData); - var newPassphrase = params.newPassphrase; - if (!email || !passphrase || !newPassphrase) { - return emailPlugin.returnError(emailPlugin.errors.INVALID_REQUEST, response); - } - emailPlugin.checkPassphrase(email, passphrase, function(error) { - if (error) { - return emailPlugin.returnError(error, response); + + var queryData = ''; + request.on('data', function(data) { + queryData += data; + if (queryData.length > MAX_ALLOWED_STORAGE) { + queryData = ''; + response.writeHead(413, { + 'Content-Type': 'text/plain' + }).end(); + request.connection.destroy(); + } + }).on('end', function() { + var params = querystring.parse(queryData); + var newPassphrase = params.newPassphrase; + if (!newPassphrase) { + return emailPlugin.returnError(emailPlugin.errors.INVALID_REQUEST, response); } emailPlugin.savePassphrase(email, newPassphrase, function(error) { if (error) { @@ -589,7 +690,23 @@ }; + // // Backwards compatibility + // + + emailPlugin.oldRetrieveDataByEmailAndPassphrase = function(email, key, passphrase, callback) { + emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { + if (err) { + return callback(err); + } + if (matches) { + return emailPlugin.retrieveByEmailAndKey(email, key, callback); + } else { + return callback(emailPlugin.errors.INVALID_CODE); + } + }); + }; + emailPlugin.oldRetrieve = function(request, response) { var email = request.param('email'); @@ -599,7 +716,7 @@ return emailPlugin.returnError(emailPlugin.errors.MISSING_PARAMETER, response); } - emailPlugin.retrieveDataByEmailAndPassphrase(email, key, secret, function(err, value) { + emailPlugin.oldRetrieveDataByEmailAndPassphrase(email, key, secret, function(err, value) { if (err) { return emailPlugin.returnError(err, response); } diff --git a/test/test.EmailStore.js b/test/test.EmailStore.js index 4cedf13c..ffe2229d 100644 --- a/test/test.EmailStore.js +++ b/test/test.EmailStore.js @@ -377,42 +377,171 @@ describe('emailstore test', function() { }); }); + describe('removing items', function() { + var fakeEmail = 'fake@email.com'; + var fakeKey = 'nameForData'; + beforeEach(function() { + leveldb_stub.del = sinon.stub(); + }); + it('deletes a stored element (key)', function(done) { + leveldb_stub.del.onFirstCall().callsArg(1); + plugin.deleteByEmailAndKey(fakeEmail, fakeKey, function(err) { + expect(err).to.be.undefined; + done(); + }); + }); + it('returns NOT FOUND if trying to delete a stored element by key', function(done) { + leveldb_stub.del.onFirstCall().callsArgWith(1, {notFound: true}); + plugin.deleteByEmailAndKey(fakeEmail, fakeKey, function(err) { + err.should.equal(plugin.errors.NOT_FOUND); + done(); + }); + }); + it('returns INTERNAL_ERROR if an unexpected error ocurrs', function(done) { + leveldb_stub.del.onFirstCall().callsArgWith(1, {unexpected: true}); + plugin.deleteByEmailAndKey(fakeEmail, fakeKey, function(err) { + err.should.equal(plugin.errors.INTERNAL_ERROR); + done(); + }); + }); + it('can delete a whole profile (validation data and passphrase)', function(done) { + leveldb_stub.del.callsArg(1); + plugin.deleteWholeProfile(fakeEmail, function(err) { + expect(err).to.be.undefined; + leveldb_stub.del.callCount.should.equal(3); + done(); + }); + }); + it('dismisses not found errors', function(done) { + leveldb_stub.del.callsArg(1); + leveldb_stub.del.onSecondCall().callsArgWith(1, {notFound: true}); + plugin.deleteWholeProfile(fakeEmail, function(err) { + expect(err).to.be.undefined; + done(); + }); + }); + it('returns internal error if something goes awry', function(done) { + leveldb_stub.del.callsArg(1); + leveldb_stub.del.onSecondCall().callsArgWith(1, {unexpected: true}); + plugin.deleteWholeProfile(fakeEmail, function(err) { + err.should.equal(plugin.errors.INTERNAL_ERROR); + done(); + }); + }); + }); + describe('when retrieving data', function() { it('should validate the secret and return the data', function() { - request.header = sinon.stub(); - request.header.onFirstCall().returns(new Buffer('email:pass', 'utf8').toString('base64')); request.param.onFirstCall().returns('key'); - plugin.retrieveDataByEmailAndPassphrase = sinon.stub(); - plugin.retrieveDataByEmailAndPassphrase.onFirstCall().callsArgWith(3, null, 'encrypted'); + plugin.authorizeRequestWithKey = sinon.stub().callsArgWith(1,null, 'email','key'); + plugin.retrieveByEmailAndKey = sinon.stub().yields(null, 'encrypted'); + response.send.onFirstCall().returnsThis(); plugin.addValidationHeader = sinon.stub().callsArg(2); plugin.retrieve(request, response); - request.header.calledOnce.should.equal(true); response.send.calledOnce.should.equal(true); - assert(request.header.firstCall.args[0] === 'authorization'); - assert(plugin.retrieveDataByEmailAndPassphrase.firstCall.args[0] === 'email'); - assert(plugin.retrieveDataByEmailAndPassphrase.firstCall.args[1] === 'key'); - assert(plugin.retrieveDataByEmailAndPassphrase.firstCall.args[2] === 'pass'); + assert(plugin.retrieveByEmailAndKey.firstCall.args[0] === 'email'); + assert(plugin.retrieveByEmailAndKey.firstCall.args[1] === 'key'); assert(response.send.firstCall.args[0] === 'encrypted'); assert(response.end.calledOnce); }); }); - describe('changing the user password', function() { - - var originalCredentials = plugin.getCredentialsFromRequest; + describe('authorizing requests', function() { + var originalCredentials; beforeEach(function() { + originalCredentials = plugin.getCredentialsFromRequest; + plugin.getCredentialsFromRequest = sinon.mock(); plugin.getCredentialsFromRequest.onFirstCall().returns({ email: 'email', - passphrase: 'passphrase' + passphrase: 'pass' }); + request.param.onFirstCall().returns('key'); + + request.on = sinon.stub(); + request.on.onFirstCall().callsArgWith(1, 'newPassphrase=newPassphrase'); + request.on.onFirstCall().returns(request); + request.on.onSecondCall().callsArg(1); + plugin.checkPassphrase = sinon.stub().callsArgWith(2,null, true); + + }); + + it('should authorize a request', function(done){ + plugin.authorizeRequest(request, false, function(err, email, key) { + expect(err).to.be.null; + expect(key).to.be.undefined; + email.should.be.equal('email'); + done(); + }); + }); + it('should authorize a request with key', function(done){ + plugin.getCredentialsFromRequest.onFirstCall().returns({ + email: 'email', + passphrase: 'pass', + }); + plugin.authorizeRequest(request, true, function(err, email, key) { + expect(err).to.be.null; + email.should.be.equal('email'); + key.should.be.equal('key'); + done(); + }); + }); + + it('should not authorize a request when param are missing', function(done){ + plugin.getCredentialsFromRequest.onFirstCall().returns({ + email: 'email', + }); + + plugin.authorizeRequest(request, false, function(err, email, key) { + expect(err).not.to.be.null; + expect(key).to.be.undefined; + expect(email).to.be.undefined; + done(); + }); + }); + it('should not authorize a request when param are missing (case2)', function(done){ + plugin.getCredentialsFromRequest.onFirstCall().returns({ + passphrase: 'pass' + }); + + plugin.authorizeRequest(request, false, function(err, email, key) { + expect(err).not.to.be.null; + expect(key).to.be.undefined; + expect(email).to.be.undefined; + done(); + }); + }); + it('should not authorize a request when param are missing (case3)', function(done){ + request.param.onFirstCall().returns(undefined); + plugin.getCredentialsFromRequest.onFirstCall().returns({ + email: 'email', + passphrase: 'pass' + }); + plugin.authorizeRequest(request, true, function(err, email, key) { + expect(err).not.to.be.null; + expect(key).to.be.undefined; + expect(email).to.be.undefined; + done(); + }); + }); + + + after(function() { + plugin.getCredentialsFromRequest = originalCredentials; + }); + }); + + describe('changing the user password', function() { + + + beforeEach(function() { request.on = sinon.stub(); request.on.onFirstCall().callsArgWith(1, 'newPassphrase=newPassphrase'); request.on.onFirstCall().returns(request); @@ -425,7 +554,7 @@ describe('emailstore test', function() { it('should validate the previous passphrase', function() { response.status.onFirstCall().returnsThis(); response.json.onFirstCall().returnsThis(); - plugin.checkPassphrase.onFirstCall().callsArgWith(2, 'error'); + plugin.authorizeRequestWithoutKey = sinon.stub().callsArgWith(1,'error'); plugin.changePassphrase(request, response); @@ -436,6 +565,7 @@ describe('emailstore test', function() { it('should change the passphrase', function() { response.json.onFirstCall().returnsThis(); + plugin.authorizeRequestWithoutKey = sinon.stub().callsArgWith(1,null, 'email'); plugin.checkPassphrase.onFirstCall().callsArgWith(2, null); plugin.savePassphrase.onFirstCall().callsArgWith(2, null); @@ -443,9 +573,5 @@ describe('emailstore test', function() { assert(response.json.calledOnce); assert(response.end.calledOnce); }); - - after(function() { - plugin.getCredentialsFromRequest = originalCredentials; - }); }); });