diff --git a/lib/db.js b/lib/db.js index 3521c2ccc4cffd5cbaaf9e75312f1d0debdb0008..7caf732f50790814dd69b15684ffbbab5df8ebd1 100644 --- a/lib/db.js +++ b/lib/db.js @@ -131,7 +131,8 @@ exports.onReady = function(f) { 'removeEmail', 'cancelAccount', 'updatePassword', - 'createUserWithPrimaryEmail' + 'createUserWithPrimaryEmail', + 'addPrimaryEmailToAccount' ].forEach(function(fn) { exports[fn] = function() { if (!config.get('database').may_write) { diff --git a/lib/db/json.js b/lib/db/json.js index 4af6a54536730b1eb8d1291dcdd9b64b1af0e6f3..9d0133880c31a01a545ab9dbac3b4047866598f3 100644 --- a/lib/db/json.js +++ b/lib/db/json.js @@ -166,14 +166,19 @@ exports.emailsBelongToSameAccount = function(lhs, rhs, cb) { }); }; -function addEmailToAccount(existing_email, email, cb) { +function addEmailToAccount(existing_email, email, type, cb) { + // validate 'type' isn't bogus + if ([ 'secondary', 'primary' ].indexOf(type) === -1) { + return process.nextTick(function() { + cb("invalid type"); + }); + } + emailToUserID(existing_email, function(userID) { if (userID == undefined) { cb("no such email: " + existing_email, undefined); } else { - db.users[userID].emails[email] = { - type: 'secondary' - }; + db.users[userID].emails[email] = { type: type }; flush(); cb(); } @@ -280,7 +285,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) { } else if (o.type === 'add_email') { exports.emailKnown(o.email, function(known) { function addIt() { - addEmailToAccount(o.existing_email, o.email, cb); + addEmailToAccount(o.existing_email, o.email, 'secondary', cb); } if (known) { exports.removeEmail(o.email, o.email, function (err) { @@ -296,6 +301,24 @@ exports.gotVerificationSecret = function(secret, hash, cb) { } }; +exports.addPrimaryEmailToAccount = function(acctEmail, emailToAdd, cb) { + sync(); + + exports.emailKnown(emailToAdd, function(known) { + function addIt() { + addEmailToAccount(acctEmail, emailToAdd, 'primary', cb); + } + if (known) { + exports.removeEmail(emailToAdd, emailToAdd, function (err) { + if (err) cb(err); + else addIt(); + }); + } else { + addIt(); + } + }); +}; + exports.checkAuth = function(email, cb) { sync(); var m = jsel.match(":root > object:has(.emails > ." + ESC(email) + ") > .password", db.users); diff --git a/lib/db/mysql.js b/lib/db/mysql.js index 7c46cd64b795aa25a9e442ccc2f6c4a7adf0065d..660de0b5afda46a7920162963c7174fdf215020a 100644 --- a/lib/db/mysql.js +++ b/lib/db/mysql.js @@ -298,6 +298,29 @@ exports.verificationSecretForEmail = function(email, cb) { }); }; +function addEmailToUser(userID, email, type, cb) { + // issue #170 - delete any old records with the same + // email address. this is necessary because + // gotVerificationSecret is invoked both for + // forgotten password flows and for new user signups. + client.query( + "DELETE FROM email WHERE address = ?", + [ email ], + function(err, info) { + if (err) { logUnexpectedError(err); cb(err); return; } + else { + client.query( + "INSERT INTO email(user, address, type) VALUES(?, ?, ?)", + [ userID, email, type ], + function(err, info) { + if (err) logUnexpectedError(err); + cb(err ? err : undefined, email); + }); + } + }); +} + + exports.gotVerificationSecret = function(secret, hash, cb) { client.query( "SELECT * FROM staged WHERE secret = ?", [ secret ], @@ -309,30 +332,6 @@ exports.gotVerificationSecret = function(secret, hash, cb) { else { var o = rows[0]; - function addEmailToUser(userID) { - // issue #170 - delete any old records with the same - // email address. this is necessary because - // gotVerificationSecret is invoked both for - // forgotten password flows and for new user signups. - // We could add an `ON DUPLICATE KEY` clause, however - // We actually want to invalidate all old public keys. - client.query( - "DELETE FROM email WHERE address = ?", - [ o.email ], - function(err, info) { - if (err) { logUnexpectedError(err); cb(err); return; } - else { - client.query( - "INSERT INTO email(user, address) VALUES(?, ?)", - [ userID, o.email ], - function(err, info) { - if (err) logUnexpectedError(err); - cb(err ? err : undefined, o.email); - }); - } - }); - } - // delete the record client.query("DELETE LOW_PRIORITY FROM staged WHERE secret = ?", [ secret ]); @@ -343,7 +342,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) { [ hash ], function(err, info) { if (err) { logUnexpectedError(err); cb(err); return; } - addEmailToUser(info.insertId); + addEmailToUser(info.insertId, o.email, 'secondary', cb); }); } else { // we're adding an email address to an existing user account. add appropriate entries into @@ -354,7 +353,7 @@ exports.gotVerificationSecret = function(secret, hash, cb) { if (err) { logUnexpectedError(err); cb(err); } else if (rows.length === 0) cb("cannot find email address: " + o.existing); else { - addEmailToUser(rows[0].user); + addEmailToUser(rows[0].user, o.email, 'secondary', cb); } }); } @@ -363,6 +362,20 @@ exports.gotVerificationSecret = function(secret, hash, cb) { ); } +exports.addPrimaryEmailToAccount = function(acctEmail, emailToAdd, cb) { + // we're adding an email address to an existing user account. add appropriate entries into + // email table + client.query( + "SELECT user FROM email WHERE address = ?", [ acctEmail ], + function(err, rows) { + if (err) { logUnexpectedError(err); cb(err); } + else if (rows.length === 0) cb("cannot find email address: " + acctEmail); + else { + addEmailToUser(rows[0].user, emailToAdd, 'primary', cb); + } + }); +} + exports.createUserWithPrimaryEmail = function(email, cb) { // create a new user acct with no password client.query( diff --git a/tests/db-test.js b/tests/db-test.js index f917c67b7622ded99c930948f712956eae033194..f9ae288704b614a7b0c6a7b5cc79c1c22ca91cb6 100755 --- a/tests/db-test.js +++ b/tests/db-test.js @@ -245,7 +245,6 @@ suite.addBatch({ } }); - suite.addBatch({ "emailType of lloyd@anywhe.re": { topic: function() { @@ -293,35 +292,43 @@ suite.addBatch({ }); suite.addBatch({ - "canceling an account": { + "creating a primary account": { topic: function() { - db.cancelAccount("lloyd@somewhe.re", this.callback); + db.createUserWithPrimaryEmail("lloyd@primary.domain", this.callback); }, "returns no error": function(r) { assert.isUndefined(r); }, "causes emailKnown": { topic: function() { - db.emailKnown('lloyd@somewhe.re', this.callback); + db.emailKnown('lloyd@primary.domain', this.callback); }, - "to return false": function (r) { - assert.strictEqual(r, false); + "to return true": function (r) { + assert.strictEqual(r, true); + } + }, + "causes emailType": { + topic: function() { + db.emailType('lloyd@primary.domain', this.callback); + }, + "to return 'primary'": function (r) { + assert.strictEqual(r, 'primary'); } } } }); suite.addBatch({ - "creating a primary account": { + "adding a primary email to that account": { topic: function() { - db.createUserWithPrimaryEmail("lloyd@primary.domain", this.callback); + db.addPrimaryEmailToAccount("lloyd@primary.domain", "lloyd2@primary.domain", this.callback); }, "returns no error": function(r) { assert.isUndefined(r); }, "causes emailKnown": { topic: function() { - db.emailKnown('lloyd@primary.domain', this.callback); + db.emailKnown('lloyd2@primary.domain', this.callback); }, "to return true": function (r) { assert.strictEqual(r, true); @@ -335,6 +342,92 @@ suite.addBatch({ assert.strictEqual(r, 'primary'); } } + }, + "adding a primary email to an account with only secondaries": { + topic: function() { + db.addPrimaryEmailToAccount("lloyd@somewhe.re", "lloyd3@primary.domain", this.callback); + }, + "returns no error": function(r) { + assert.isUndefined(r); + }, + "causes emailKnown": { + topic: function() { + db.emailKnown('lloyd3@primary.domain', this.callback); + }, + "to return true": function (r) { + assert.strictEqual(r, true); + } + }, + "causes emailType": { + topic: function() { + db.emailType('lloyd3@primary.domain', this.callback); + }, + "to return 'primary'": function (r) { + assert.strictEqual(r, 'primary'); + } + } + } +}); + +suite.addBatch({ + "adding a registered primary email to an account": { + topic: function() { + db.addPrimaryEmailToAccount("lloyd@primary.domain", "lloyd3@primary.domain", this.callback); + }, + "returns no error": function(r) { + assert.isUndefined(r); + }, + "and emailKnown": { + topic: function() { + db.emailKnown('lloyd3@primary.domain', this.callback); + }, + "still returns true": function (r) { + assert.strictEqual(r, true); + } + }, + "and emailType": { + topic: function() { + db.emailType('lloyd@primary.domain', this.callback); + }, + "still returns 'primary'": function (r) { + assert.strictEqual(r, 'primary'); + } + }, + "and email is removed": { + topic: function() { + db.emailsBelongToSameAccount('lloyd3@primary.domain', 'lloyd@somewhe.re', this.callback); + }, + "from original account": function(r) { + assert.isFalse(r); + } + }, + "and email is added": { + topic: function() { + db.emailsBelongToSameAccount('lloyd3@primary.domain', 'lloyd@primary.domain', this.callback); + }, + "to new account": function(r) { + assert.isTrue(r); + } + } + } +}); + +suite.addBatch({ + "canceling an account": { + topic: function() { + db.cancelAccount("lloyd@somewhe.re", this.callback); + }, + "returns no error": function(r) { + assert.isUndefined(r); + }, + "causes emailKnown": { + topic: function() { + db.emailKnown('lloyd@somewhe.re', this.callback); + }, + "to return false": function (r) { + assert.strictEqual(r, false); + } + } } });