sessionstore
Version:
Sessionstore is a node.js module for multiple databases. It can be very useful if you work with express or connect.
277 lines (229 loc) • 7.12 kB
JavaScript
var util = require('util'),
Session = require('../sessionInterface'),
use = require('../use'),
mongo = use('mongodb'),
mongoVersion = use('mongodb/package.json').version,
isNew = mongoVersion.indexOf('1.') !== 0,
ObjectID = isNew ? mongo.ObjectID : mongo.BSONPure.ObjectID,
_ = require('lodash');
function cleanSessionData(json) {
if (!json) {
return json;
}
var data = {};
for (var i in json) {
data[i] = json[i];
if (data[i] instanceof Object) {
if ('low_' in data[i] || 'high_' in data[i]) {
data[i] = data[i].toNumber();
}
}
}
return data;
}
var MongoSessionStore = function (options) {
options = options || {};
Session.Store.call(this, options);
var defaults = {
host: 'localhost',
port: 27017,
dbName: 'express-sessions',
collectionName: 'sessions',
// heartbeat: 60 * 1000,
ttl: 60 * 60 * 24 * 14 // 14 days
};
_.defaults(options, defaults);
var defaultOpt = {
ssl: false
};
options.options = options.options || {};
if (isNew) {
defaultOpt.autoReconnect = false;
defaultOpt.useNewUrlParser = true;
defaultOpt.useUnifiedTopology = true;
_.defaults(options.options, defaultOpt);
} else {
defaultOpt.auto_reconnect = false;
_.defaults(options.options, defaultOpt);
}
this.options = options;
};
util.inherits(MongoSessionStore, Session.Store);
_.extend(MongoSessionStore.prototype, {
connect: function (callback) {
var self = this;
var options = this.options;
var connectionUrl;
if (options.url) {
connectionUrl = options.url;
} else {
var members = options.servers
? options.servers
: [{host: options.host, port: options.port}];
var memberString = _(members).map(function(m) { return m.host + ':' + m.port; });
var authString = options.username && options.password
? options.username + ':' + options.password + '@'
: '';
var optionsString = options.authSource
? '?authSource=' + options.authSource
: '';
connectionUrl = 'mongodb://' + authString + memberString + '/' + options.dbName + optionsString;
}
var client;
if (mongo.MongoClient.length === 2) {
client = new mongo.MongoClient(connectionUrl, options.options);
client.connect(function(err, cl) {
if (err) {
if (callback) callback(err);
return;
}
self.db = cl.db(cl.s.options.dbName);
if (!self.db.close) {
self.db.close = cl.close.bind(cl);
}
initDb();
});
} else {
client = new mongo.MongoClient();
client.connect(connectionUrl, options.options, function(err, db) {
if (err) {
if (callback) callback(err);
return;
}
self.db = db;
initDb();
});
}
function initDb() {
self.db.on('close', function() {
self.isConnected = false;
self.emit('disconnect');
self.stopHeartbeat();
});
var finish = function (err) {
self.sessions = self.db.collection(options.collectionName);
self.sessions.ensureIndex({ expires: 1 }, { expireAfterSeconds: 0 }, function() {});
if (!err) {
self.isConnected = true;
self.emit('connect');
if (self.options.heartbeat) {
self.startHeartbeat();
}
}
if (callback) callback(err, self);
};
finish();
}
},
stopHeartbeat: function () {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
delete this.heartbeatInterval;
}
},
startHeartbeat: function () {
var self = this;
var gracePeriod = Math.round(this.options.heartbeat / 2);
this.heartbeatInterval = setInterval(function () {
var graceTimer = setTimeout(function () {
if (self.heartbeatInterval) {
console.error((new Error ('Heartbeat timeouted after ' + gracePeriod + 'ms (mongodb)')).stack);
self.disconnect();
}
}, gracePeriod);
self.db.command({ ping: 1 }, function (err) {
if (graceTimer) clearTimeout(graceTimer);
if (err) {
console.error(err.stack || err);
self.disconnect();
}
});
}, this.options.heartbeat);
},
disconnect: function (callback) {
this.stopHeartbeat();
if (!this.db || !this.isConnected) {
if (callback) callback(null);
return;
}
this.db.close(function (err) {
if (callback) callback(err);
});
},
set: function (sid, sess, callback) {
if (sess && sess.cookie && sess.cookie.expires) {
sess.expires = new Date(sess.cookie.expires);
} else {
// If there's no expiration date specified, it is
// browser-session cookie or there is no cookie at all,
// as per the connect docs.
//
// So we set the expiration to two-weeks from now
// - as is common practice in the industry (e.g Django) -
// or the default specified in the options.
sess.expires = new Date(Date.now() + this.options.ttl * 1000);
}
sess._id = sid;
var query = {
_id: sid
};
if (!this.options.ignoreConcurrency && sess._hash) {
query._hash = sess._hash;
}
if (!this.options.ignoreConcurrency) sess._hash = new ObjectID().toString();
this.sessions.update(query, sess, { upsert: true, safe: true }, function(err, modifiedCount) {
if (err && err.message && err.message.indexOf('duplicate key') > -1) {
return callback(new Error('ConcurrencyError: Session was updated by someone else!'));
}
if (modifiedCount && modifiedCount.result && modifiedCount.result.n === 0) {
return callback(new Error('ConcurrencyError: Session was updated by someone else!'));
}
if (err) {
if (callback) callback(err);
return;
}
if (callback) callback(null, sess);
});
},
//touch: function (sid, sess, callback) {
// this.set(sid, sess, callback);
//},
get: function (sid, callback) {
this.sessions.findOne({
_id: sid,
$or: [
{ expires: { $exists: false } },
{ expires: { $gt: new Date() } }
]
}, function (err, session_data) {
if (err) {
if (callback) callback(err);
return;
}
session_data = cleanSessionData(session_data);
if (callback) callback(null, session_data);
});
},
destroy: function (sid, callback) {
this.sessions.remove({ _id: sid }, callback || function () {});
},
length: function (callback) {
this.sessions.count(callback || function () {});
},
all: function (callback) {
var arr = [];
this.sessions.find(function (err, cursor) {
cursor.each(function (err, d) {
d = cleanSessionData(d);
arr.push(d);
if (!d._id) {
if (callback) callback(null, arr);
}
});
});
},
clear: function (callback) {
this.sessions.remove(callback || function () {});
}
});
module.exports = MongoSessionStore;