274 lines
7.1 KiB
JavaScript
274 lines
7.1 KiB
JavaScript
|
'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;
|
||
|
}
|
||
|
}
|