Merge pull request #272 from isocolsky/expire_email

[EmailStorage] Added expiration date to pending email registration
This commit is contained in:
Matias Alejo Garcia 2014-12-11 14:29:21 -03:00
commit cd7c344125
2 changed files with 90 additions and 28 deletions

View File

@ -13,6 +13,7 @@
var levelup = require('levelup'); var levelup = require('levelup');
var nodemailer = require('nodemailer'); var nodemailer = require('nodemailer');
var querystring = require('querystring'); var querystring = require('querystring');
var moment = require('moment');
var logger = require('../lib/logger').logger; var logger = require('../lib/logger').logger;
var globalConfig = require('../config/config'); var globalConfig = require('../config/config');
@ -50,7 +51,11 @@
OVER_QUOTA: { OVER_QUOTA: {
code: 406, code: 406,
message: 'User quota exceeded', message: 'User quota exceeded',
} },
REGISTRATION_EXPIRED: {
code: 400,
message: 'Registration expired',
},
}; };
var EMAIL_TO_PASSPHRASE = 'email-to-passphrase-'; var EMAIL_TO_PASSPHRASE = 'email-to-passphrase-';
@ -69,6 +74,8 @@
var POST_LIMIT = 1024 * 300 /* Max POST 300 kb */ ; var POST_LIMIT = 1024 * 300 /* Max POST 300 kb */ ;
var DAYS_TO_EXPIRATION = 7; // An email can be awaiting validation for this long before expiring
var valueKey = function(email, key) { var valueKey = function(email, key) {
return STORED_VALUE + bitcore.util.twoSha256(email + SEPARATOR + key).toString('hex'); return STORED_VALUE + bitcore.util.twoSha256(email + SEPARATOR + key).toString('hex');
}; };
@ -361,9 +368,20 @@
*/ */
emailPlugin.createVerificationSecret = function(email, callback) { emailPlugin.createVerificationSecret = function(email, callback) {
emailPlugin.db.get(pendingKey(email), function(err, value) { emailPlugin.db.get(pendingKey(email), function(err, value) {
if (err && err.notFound) { var available = false;
var notFound = err && err.notFound;
var expired = !err && _.isObject(value) && moment().unix() > value.expires;
var available = notFound || expired;
if (available) {
var secret = emailPlugin.crypto.randomBytes(16).toString('hex'); var secret = emailPlugin.crypto.randomBytes(16).toString('hex');
emailPlugin.db.put(pendingKey(email), secret, function(err) { var value = {
secret: secret,
expires: moment().add(DAYS_TO_EXPIRATION, 'days').unix(),
};
emailPlugin.db.put(pendingKey(email), value, function(err) {
if (err) { if (err) {
logger.error('error saving pending data:', email, secret); logger.error('error saving pending data:', email, secret);
return callback(emailPlugin.errors.INTERNAL_ERROR); return callback(emailPlugin.errors.INTERNAL_ERROR);
@ -741,29 +759,39 @@
code: 500, code: 500,
message: err message: err
}, response); }, response);
} else if (value !== secret) {
return emailPlugin.returnError(emailPlugin.errors.INVALID_CODE, response);
} else {
emailPlugin.db.put(validatedKey(email), true, function(err, value) {
if (err) {
return emailPlugin.returnError({
code: 500,
message: err
}, response);
} else {
emailPlugin.db.del(pendingKey(email), function(err, value) {
if (err) {
return emailPlugin.returnError({
code: 500,
message: err
}, response);
} else {
response.redirect(emailPlugin.redirectUrl);
}
});
}
});
} }
if (_.isObject(value)) {
if (moment().unix() > value.expires) {
return emailPlugin.returnError(emailPlugin.errors.REGISTRATION_EXPIRED, response);
} else {
value = value.secret;
}
}
if (value !== secret) {
return emailPlugin.returnError(emailPlugin.errors.INVALID_CODE, response);
}
emailPlugin.db.put(validatedKey(email), true, function(err, value) {
if (err) {
return emailPlugin.returnError({
code: 500,
message: err
}, response);
} else {
emailPlugin.db.del(pendingKey(email), function(err, value) {
if (err) {
return emailPlugin.returnError({
code: 500,
message: err
}, response);
} else {
response.redirect(emailPlugin.redirectUrl);
}
});
}
});
}); });
}; };

View File

@ -8,6 +8,7 @@ var bitcore = require('bitcore');
var logger = require('../lib/logger').logger; var logger = require('../lib/logger').logger;
var should = chai.should; var should = chai.should;
var expect = chai.expect; var expect = chai.expect;
var moment = require('moment');
logger.transports.console.level = 'non'; logger.transports.console.level = 'non';
@ -225,9 +226,12 @@ describe('emailstore test', function() {
it('saves data under the expected key', function(done) { it('saves data under the expected key', function(done) {
setupLevelDb(); setupLevelDb();
var clock = sinon.useFakeTimers();
plugin.createVerificationSecretAndSendEmail(fakeEmail, function(err) { plugin.createVerificationSecretAndSendEmail(fakeEmail, function(err) {
leveldb_stub.put.firstCall.args[1].should.equal(fakeRandom); var arg = leveldb_stub.put.firstCall.args[1];
arg.secret.should.equal(fakeRandom);
arg.expires.should.equal(moment().add(7, 'days').unix());
clock.restore();
done(); done();
}); });
}); });
@ -363,7 +367,7 @@ describe('emailstore test', function() {
response.json.returnsThis(); response.json.returnsThis();
}); });
it('should validate correctly an email if the secret matches', function() { it('should validate correctly an email if the secret matches (without expiration date)', function() {
leveldb_stub.get.onFirstCall().callsArgWith(1, null, secret); leveldb_stub.get.onFirstCall().callsArgWith(1, null, secret);
leveldb_stub.del = sinon.stub().yields(null); leveldb_stub.del = sinon.stub().yields(null);
response.redirect = sinon.stub(); response.redirect = sinon.stub();
@ -373,6 +377,19 @@ describe('emailstore test', function() {
assert(response.redirect.firstCall.calledWith(plugin.redirectUrl)); assert(response.redirect.firstCall.calledWith(plugin.redirectUrl));
}); });
it('should validate correctly an email if the secret matches (using expiration date)', function() {
leveldb_stub.get.onFirstCall().callsArgWith(1, null, {
secret: secret,
expires: moment().add(7, 'days').unix(),
});
leveldb_stub.del = sinon.stub().yields(null);
response.redirect = sinon.stub();
plugin.validate(request, response);
assert(response.redirect.firstCall.calledWith(plugin.redirectUrl));
});
it('should fail to validate an email if the secret doesn\'t match', function() { it('should fail to validate an email if the secret doesn\'t match', function() {
var invalid = '3'; var invalid = '3';
leveldb_stub.get.onFirstCall().callsArgWith(1, null, invalid); leveldb_stub.get.onFirstCall().callsArgWith(1, null, invalid);
@ -387,6 +404,23 @@ describe('emailstore test', function() {
})); }));
assert(response.end.calledOnce); assert(response.end.calledOnce);
}); });
it('should fail to validate an email if the secret has expired', function() {
leveldb_stub.get.onFirstCall().callsArgWith(1, null, {
secret: secret,
expires: moment().subtract(2, 'days').unix(),
});
response.status.returnsThis();
response.json.returnsThis();
plugin.validate(request, response);
assert(response.status.firstCall.calledWith(plugin.errors.REGISTRATION_EXPIRED.code));
assert(response.json.firstCall.calledWith({
error: 'Registration expired'
}));
assert(response.end.calledOnce);
});
}); });
describe('removing items', function() { describe('removing items', function() {