UNPKG

koa-atomic-session

Version:
305 lines (258 loc) 7.15 kB
'use strict' const wrap = require('mongodb-next').collection const ObjectID = require('mongodb').ObjectID const assert = require('assert') const CSRF = require('csrf') const ms = require('ms') module.exports = function (app, options) { assert(app.middleware, 'First argument must be the app.') options = options || {} // CSRF options const csrf = CSRF(options) // the cookie name const key = options.key || 'sid' /** * Wrap the collection in a `mongodb-next` instance. */ Object.defineProperty(Session, 'collection', { get: function () { assert(this._collection, 'Collection not set!') return this._collection }, set: function (collection) { this._rawCollection = collection this._collection = wrap(collection) }, }) // set the collection if already defined if (options.collection) Session.collection = options.collection /** * Create a TTL index for expirations. */ Session.ensureIndex = function () { return Session.collection.ensureIndex({ expires: 1, }, { expireAfterSeconds: 0, background: true, }) } /** * Usage: * * const session = yield this.session() */ app.context.session = function () { // session is current being queried if (this._sessionPromise) return this._sessionPromise // session already queried if (this._session) return Promise.resolve(this._session) // get the session const self = this return this._sessionPromise = Promise.resolve().then(function () { const session = self._session = new Session(self) const sid = session.cookies.get(key, session) if (!/^[0-9a-f]{24}$/i.test(sid)) return session._create() return Session.collection.findOne(new ObjectID(sid)) .then(function (obj) { // non-existent session if (!obj) return session._create() // set all the properties locally Object.keys(obj).forEach(function (key) { session[key] = obj[key] }) return session }) }).then(function (session) { delete self._sessionPromise return session }) } /** * Session constructor. */ function Session(context) { this.context = context this.cookies = context.cookies this.update = this.update.bind(this) } /** * Get and set the `maxAge`. */ Object.defineProperty(Session.prototype, 'maxAge', { get: function () { return this._maxAge }, set: function (maxAge) { if (typeof maxAge === 'string') maxAge = ms(maxAge) assert(typeof maxAge === 'number') this._maxAge = maxAge }, }) /** * Cookie options. */ Session.prototype.overwrite = true Session.prototype.httpOnly = true Session.prototype.signed = true // should be encrypted later Session.prototype.maxAge = options.maxage || options.maxAge || '14 days' /** * .id is just a shorthand for ._id */ Object.defineProperty(Session.prototype, 'id', { get: function () { return this._id }, }) /** * Destroy the current session. * * const session = yield this.session() * yield session.destroy() * */ Session.prototype.destroy = function () { this.cookies.set(key, '', this) delete this.context._session return Promise.resolve(this._id && Session.collection.findOne(this._id).remove()) .then(noop) } /** * Destroy the current session and create a new one. * * const session = yield this.session() * session = yield session.regenerate() * */ Session.prototype.regenerate = function () { const context = this.context return this.destroy().then(function () { const session = context._session = new Session(context) return session._create() }) } /** * Update the expires age for the cookie as well as the session. * * const session = yield this.session() * yield session.touch() * */ Session.prototype.touch = function () { this._setSession() return Session.collection.findOne(this._id) .set('maxAge', this.maxAge) .set('expires', this.expires) .new() } /** * Create a CSRF token. * * const session = yield this.session() * const csrf = session.createCSRF() * */ Session.prototype.createCSRF = function () { assert(this.secret) return csrf.create(this.secret) } /** * Check whether a CSRF token is valid. * * const session = yield this.session() * session.assertCSRF(this.request.get('X-CSRF-Token')) * */ Session.prototype.assertCSRF = function (val) { this.context.assert(csrf.verify(this.secret, val), 401, 'Invalid CSRF Token.') } /** * Command entry points. * * const session = yield this.session() * yield session.set('a', 'b').unset('c').push('d', 1) */ const commands = [ 'addToSet', 'pop', 'pullAll', 'pull', 'pushAll', 'push', 'set', 'inc', 'unset', 'rename', ] commands.forEach(function (command) { Session.prototype[command] = function () { const query = this.touch() return query[command].apply(query, arguments) } }) /** * Update the session object with the results from an update. * Ideally, this would be automatic, but I haven't figured that out yet. * * const session = yield this.session() * yield session.set('a', 'b').then(session.update) * assert(session.a === 'b') * */ Session.prototype.update = function (session) { if (!session) return this const keys = Object.keys(this).filter(function (key) { // we have to manually ignore some keys switch (key) { case 'id': case 'context': case 'cookies': case '_maxAge': case 'update': return false } return true }) const newkeys = Object.keys(session) newkeys.forEach(function (key) { this[key] = session[key] const i = keys.indexOf(key) if (~i) keys.splice(i, 1) }, this) // removed shift keys.forEach(function (key) { delete this[key] }, this) return this } /** * Set the cookie expires as well as update this.expires. * You are expected to update .expires on the session as well. */ Session.prototype._setSession = function (expires) { this.expires = expires || new Date(Date.now() + this.maxAge) this.cookies.set(key, this._id.toHexString(), this) } /** * Create a new session. */ Session.prototype._create = function () { const self = this const session = { maxAge: this.maxAge, } this._id = session._id = new ObjectID() this.expires = session.expires = new Date(Date.now() + this.maxAge) this.created = session.created = new Date() this.cookies.set(key, this._id.toHexString(), this) return csrf.secret().then(function (secret) { self.secret = session.secret = secret return Session.collection.insert(session) }).then(function () { return self }) } return Session } /* istanbul ignore next */ function noop() {}