'use strict'; const Archetype = require('archetype'); const EventEmitter = require('events').EventEmitter; const mongodb = require('mongodb'); const OptionsType = new Archetype({ uri: { $type: 'string', $required: true, $default: 'mongodb://localhost:27017/test' }, collection: { $type: 'string', $required: true, $default: 'sessions' }, connectionOptions: { $type: Object, $default: () => ({ useNewUrlParser: true, useUnifiedTopology : true }) }, expires: { $type: 'number', $required: true, $default: 1000 * 60 * 60 * 24 * 14 // 2 weeks }, idField: { $type: 'string', $required: true, $default: '_id' }, databaseName: { $type: 'string', $required: false, $default: null }, expiresKey: { $type: 'string', $required: true, $default: 'expires' }, expiresAfterSeconds: { $type: 'number', $required: true, $default: 0 } }).compile('OptionsType'); /** * Returns a constructor with the specified connect middleware's Store * class as its prototype * * ####Example: * * connectMongoDBSession(require('express-session')); * * @param {Function} connect connect-compatible session middleware (e.g. Express 3, express-session) * @api public */ module.exports = function(connect) { const Store = connect.Store || connect.session.Store; const MongoDBStore = function(options, callback) { if (!(this instanceof MongoDBStore)) { return new MongoDBStore(options, callback); } const _this = this; this._emitter = new EventEmitter(); this._errorHandler = handleError.bind(this); this.client = null; this.db = null; if (typeof options === 'function') { callback = options; options = {}; } else { options = options || {}; } options = new OptionsType(options); Store.call(this, options); this.options = options; const connOptions = options.connectionOptions; mongodb.MongoClient.connect(options.uri, connOptions, function(error, client) { if (error) { var e = new Error('Error connecting to db: ' + error.message); return _this._errorHandler(e, callback); } const db = options.databaseName == null ? client.db() : client.db(options.databaseName); _this.client = client; _this.db = db; const expiresIndex = {}; expiresIndex[options.expiresKey] = 1 db. collection(options.collection). createIndex(expiresIndex, { expireAfterSeconds: options.expiresAfterSeconds }, function(error) { if (error) { const e = new Error('Error creating index: ' + error.message); return _this._errorHandler(e, callback); } _this._emitter.emit('connected'); return callback && callback(); }); }); }; MongoDBStore.prototype = Object.create(Store.prototype); MongoDBStore.prototype._generateQuery = function(id) { const ret = {}; ret[this.options.idField] = id; return ret; }; MongoDBStore.prototype.get = function(id, callback) { const _this = this; if (!this.db) { return this._emitter.once('connected', function() { _this.get.call(_this, id, callback); }); } this.db.collection(this.options.collection). findOne(this._generateQuery(id), function(error, session) { if (error) { const e = new Error('Error finding ' + id + ': ' + error.message); return _this._errorHandler(e, callback); } else if (session) { if (!session.expires || new Date < session.expires) { return callback(null, session.session); } else { return _this.destroy(id, callback); } } else { return callback(); } }); }; // new store.all() for all sessions MongoDBStore.prototype.all = function(callback) { const _this = this; if (!this.db) { return this._emitter.once('connected', function() { _this.all.call(_this, callback); }); } this.db.collection(this.options.collection). find({}).toArray(function(error, sessions) { if (error) { const e = new Error('Error gathering sessions'); return _this._errorHandler(e, callback); } else if (sessions) { if (sessions) { return callback(null, sessions); } } else { return callback(); } }); }; MongoDBStore.prototype.destroy = function(id, callback) { const _this = this; if (!this.db) { return this._emitter.once('connected', function() { _this.destroy.call(_this, id, callback); }); } this.db.collection(this.options.collection). deleteOne(this._generateQuery(id), function(error) { if (error) { const e = new Error('Error destroying ' + id + ': ' + error.message); return _this._errorHandler(e, callback); } callback && callback(); }); }; MongoDBStore.prototype.clear = function(callback) { const _this = this; if (!this.db) { return this._emitter.once('connected', function() { _this.clear.call(_this, callback); }); } this.db.collection(this.options.collection). deleteMany({}, function(error) { if (error) { const e = new Error('Error clearing all sessions: ' + error.message); return _this._errorHandler(e, callback); } callback && callback(); }); }; MongoDBStore.prototype.set = function(id, session, callback) { const _this = this; if (!this.db) { return this._emitter.once('connected', function() { _this.set.call(_this, id, session, callback); }); } const sess = {}; for (const key in session) { if (key === 'cookie') { sess[key] = session[key].toJSON ? session[key].toJSON() : session[key]; } else { sess[key] = session[key]; } } const s = this._generateQuery(id); s.session = sess; if (session && session.cookie && session.cookie.expires) { s[this.options.expiresKey] = new Date(session.cookie.expires); } else { const now = new Date(); s[this.options.expiresKey] = new Date(now.getTime() + this.options.expires); } this.db.collection(this.options.collection). updateOne(this._generateQuery(id), { $set: s }, { upsert: true }, function(error) { if (error) { const e = new Error('Error setting ' + id + ' to ' + require('util').inspect(session) + ': ' + error.message); return _this._errorHandler(e, callback); } callback && callback(); }); }; MongoDBStore.prototype.on = function() { this._emitter.on.apply(this._emitter, arguments); }; MongoDBStore.prototype.once = function() { this._emitter.once.apply(this._emitter, arguments); }; return MongoDBStore; }; function handleError(error, callback) { if (this._emitter.listeners('error').length) { this._emitter.emit('error', error); } if (callback) { callback(error); } if (!this._emitter.listeners('error').length && !callback) { throw error; } }