From 5035b545a3f0949246eab579da753f75b926ef5a Mon Sep 17 00:00:00 2001 From: Lloyd Hilaiel <lloyd@hilaiel.com> Date: Wed, 3 Aug 2011 16:40:04 -0600 Subject: [PATCH] complete implementation of a trivial json database for local dev using jsonselect. --- browserid/lib/db_json.js | 237 ++++++++++++++++++++----------------- browserid/tests/db-test.js | 2 +- package.json | 3 +- 3 files changed, 134 insertions(+), 108 deletions(-) diff --git a/browserid/lib/db_json.js b/browserid/lib/db_json.js index 5e37c1bf0..af01b2c95 100644 --- a/browserid/lib/db_json.js +++ b/browserid/lib/db_json.js @@ -1,18 +1,51 @@ +/* db_json is a json database driver. It is designed for use in + * local development, is intended to be extremely easy to maintain, + * have minimal dependencies on 3rd party libraries, and we could + * care less if it performs well with more than 10 or so users. + */ const path = require('path'), fs = require('fs'), -secrets = require('./secrets'); +secrets = require('./secrets'), +jsel = require('JSONSelect'); + +// a little alias for stringify +const ESC = JSON.stringify; var VAR_DIR = path.join(path.dirname(__dirname), "var"); var dbPath = path.join(VAR_DIR, "authdb.json"); +/* The JSON database. The structure is thus: + * [ + * { + * password: "somepass", + * emails: [ + * { + * address: "lloyd@hilaiel.com", + * keys: [ + * { + * key: "SOMESTRINGOFTEXT", + * expires: 1231541615125 + * } + * ] + * } + * ] + * } + * ] + */ + var db = []; var stagedEmails = { }; var staged = { }; function flush() { - fs.writeFileSync(JSON.stringify(db)); + fs.writeFileSync(dbPath, JSON.stringify(db)); +} + +// when should a key created right now expire? +function getExpiryTime() { + return ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)); } exports.open = function(cfg, cb) { @@ -31,13 +64,8 @@ exports.close = function(cb) { }; exports.emailKnown = function(email, cb) { - for (var i = 0; i < db.length; i++) { - if (db[i].emails.hasOwnProperty(email)) { - setTimeout(function() { cb(true) }, 0); - return; - } - } - setTimeout(function() { cb(false) }, 0); + var m = jsel.match(".address:val(" + ESC(email) + ")", db); + setTimeout(function() { cb(m.length > 0) }, 0); }; exports.isStaged = function(email, cb) { @@ -60,6 +88,26 @@ exports.emailsBelongToSameAccount = function(lhs, rhs, cb) { }); }; +function addEmailToAccount(existing_email, email, pubkey, cb) { + emailToUserID(existing_email, function(userID) { + if (userID == undefined) { + cb("no such email: " + existing_email, undefined); + } else { + db[userID].emails.push({ + address: email, + keys: [ + { + key: pubkey, + expires: getExpiryTime() + } + ] + }); + flush(); + cb(); + } + }); +} + exports.addKeyToEmail = function(existing_email, email, pubkey, cb) { emailToUserID(existing_email, function(userID) { if (userID == undefined) { @@ -67,24 +115,28 @@ exports.addKeyToEmail = function(existing_email, email, pubkey, cb) { return; } - if (db[userID].emails - - db.execute("SELECT emails.id FROM emails,users WHERE users.id = ? AND emails.address = ? AND emails.user = users.id", - [ userID, email ], - function(err, rows) { - if (err || rows.length != 1) { - cb(err); - return; - } - executeTransaction([ - [ "INSERT INTO keys (email, key, expires) VALUES(?,?,?)", - [ rows[0].id, pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ] - ] - ], function (error) { - if (error) cb(error); - else cb(); - }); - }); + if (!(db[userID].emails)) { + db[userID].emails = [ ]; + } + + var m = jsel.match("object:has(.address:val(" + ESC(email) + ")) > .keys", db[userID].emails); + + var kobj = { + key: pubkey, + expires: getExpiryTime() + }; + + if (m.length) { + m[0].push(kobj); + } else { + db[userID].emails.push({ + address: email, + keys: [ kobj ] + }); + } + + flush(); + if (cb) setTimeout(function() { cb(); }, 0); }); } @@ -133,11 +185,12 @@ exports.gotVerificationSecret = function(secret, cb) { address: o.email, keys: [ { key: o.pubkey, - expires: ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) + expires: getExpiryTime(), } ] } ] }); + flush(); cb(); } @@ -177,22 +230,19 @@ exports.gotVerificationSecret = function(secret, cb) { }; exports.checkAuth = function(email, cb) { - db.execute("SELECT users.password FROM emails, users WHERE users.id = emails.user AND emails.address = ?", - [ email ], - function (error, rows) { - cb(rows.length !== 1 ? undefined : rows[0].password); - }); + var m = jsel.match(":root > object:has(.address:val(" + ESC(email) + ")) > .password", db); + if (m.length === 0) m = undefined; + else m = m[0]; + setTimeout(function() { cb(m) }, 0); }; function emailToUserID(email, cb) { var id = undefined; - + for (var i = 0; i < db.length; i++) { - for (var j = 0; j < db[i].emails.length; j++) { - if (db[i].emails[j].address === email) { - id = i; - break; - } + if (jsel.match(".address:val(" + JSON.stringify(email) + ")", db[i]).length) { + id = i; + break; } if (id !== undefined) break; } @@ -212,86 +262,61 @@ exports.getSyncResponse = function(email, identities, cb) { cb("no such email: " + email); return; } - db.execute( - 'SELECT address FROM emails WHERE ? = user', - [ userID ], - function (err, rows) { - if (err) cb(err); - else { - var emails = [ ]; - var keysToCheck = [ ]; - for (var i = 0; i < rows.length; i++) emails.push(rows[i].address); - - // #1 - for (var e in identities) { - if (emails.indexOf(e) == -1) respBody.unknown_emails.push(e); - else keysToCheck.push(e); - } + var emails = jsel.match(".address", db[userID]); + var keysToCheck = [ ]; - // #2 - for (var e in emails) { - e = emails[e]; - if (!identities.hasOwnProperty(e)) respBody.key_refresh.push(e); - } + // #1 emails that the client knows about but we do not + for (var e in identities) { + if (emails.indexOf(e) == -1) respBody.unknown_emails.push(e); + else keysToCheck.push(e); + } - // #3 -- yes, this is sub-optimal in terms of performance. when we - // move away from public keys this will be unnec. - if (keysToCheck.length) { - var checked = 0; - keysToCheck.forEach(function(e) { - emailHasPubkey(e, identities[e], function(v) { - checked++; - if (!v) respBody.key_refresh.push(e); - if (checked === keysToCheck.length) { - cb(undefined, respBody); - } - }); - }); - } else { - cb(undefined, respBody); - } - } + // #2 emails that we know about and the client does not + for (var e in emails) { + e = emails[e]; + if (!identities.hasOwnProperty(e)) respBody.key_refresh.push(e); + } + + // #3 emails that we both know about but who need to be re-keyed + if (keysToCheck.length) { + var checked = 0; + keysToCheck.forEach(function(e) { + if (!jsel.match(".key:val(" + ESC(identities[e]) + ")", db[userID]).length) + respBody.key_refresh.push(e); + checked++; + if (checked === keysToCheck.length) cb(undefined, respBody); }); + } else { + cb(undefined, respBody); + } }); }; exports.pubkeysForEmail = function(identity, cb) { - db.execute( - 'SELECT keys.key FROM keys, emails WHERE emails.address = ? AND keys.email = emails.id', - [ identity ], - function(err, rows) { - var keys = undefined; - if (!err && rows && rows.length) { - keys = [ ]; - for (var i = 0; i < rows.length; i++) keys.push(rows[i].key); - } - cb(keys); - }); + var m = jsel.match(".emails object:has(.address:val(" + ESC(identity)+ ")) .key", db); + setTimeout(function() { cb(m); }, 0); }; exports.removeEmail = function(authenticated_email, email, cb) { - // figure out the user, and remove Email only from addressed - // linked to the authenticated email address - emailToUserID(authenticated_email, function(user_id) { - executeTransaction([ - [ "delete from emails where emails.address = ? and user = ?", [ email,user_id ] ] , - [ "delete from keys where email in (select address from emails where emails.address = ? and user = ?)", [ email,user_id ] ], - ], function (error) { - if (error) cb(error); - else cb(); - }); - }); + var m = jsel.match(":root > object:has(.address:val("+ESC(authenticated_email)+")):has(.address:val("+ESC(email)+")) .emails", db); + + if (m.length) { + var emails = m[0]; + for (var i = 0; i < emails.length; i++) { + if (emails[i].address === email) { + emails.splice(i, 1); + break; + } + } + } + + setTimeout(function() { cb(); }, 0); }; exports.cancelAccount = function(authenticated_email, cb) { emailToUserID(authenticated_email, function(user_id) { - executeTransaction([ - [ "delete from emails where user = ?", [ user_id ] ] , - [ "delete from keys where email in (select address from emails where user = ?)", [ user_id ] ], - [ "delete from users where id = ?", [ user_id ] ], - ], function (error) { - if (error) cb(error); - else cb(); - }); + db.splice(user_id, 1); + flush(); + cb(); }); }; diff --git a/browserid/tests/db-test.js b/browserid/tests/db-test.js index 83e51fa5c..e6aece9f9 100755 --- a/browserid/tests/db-test.js +++ b/browserid/tests/db-test.js @@ -12,7 +12,7 @@ var suite = vows.describe('db'); // disable vows (often flakey?) async error behavior suite.options.error = false; -var dbPath = temp.path({suffix: '.sqlite'}); +var dbPath = temp.path({suffix: '.db'}); suite.addBatch({ "onReady": { diff --git a/package.json b/package.json index 78fdad42f..412487fdd 100644 --- a/package.json +++ b/package.json @@ -15,5 +15,6 @@ , "temp": "0.2.0" , "express-csrf": "0.3.2" , "uglify-js": "1.0.6" + , "JSONSelect": "0.2.1" } -} \ No newline at end of file +} -- GitLab