From 4700b18f36334d5c8d32982a225b5b624c5bcdbf Mon Sep 17 00:00:00 2001 From: Joe Grayauskie Date: Thu, 1 Jun 2017 04:22:56 -0400 Subject: [PATCH 01/24] =?UTF-8?q?Create=20capability=20to=20create=20cooki?= =?UTF-8?q?es=20that=20expire=20when=20browser=20is=20close=E2=80=A6=20(#7?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Readme.md | 5 ++++- lib/context.js | 17 +++++++++------ test/store.test.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/Readme.md b/Readme.md index da7e8ae..337b65e 100644 --- a/Readme.md +++ b/Readme.md @@ -47,7 +47,10 @@ app.keys = ['some secret hurr']; var CONFIG = { key: 'koa:sess', /** (string) cookie key (default is koa:sess) */ - maxAge: 86400000, /** (number) maxAge in ms (default is 1 days) */ + /** (number || 'session') maxAge in ms (default is 1 days) */ + /** 'session' will result in a cookie that expires when session/browser is closed */ + /** Warning: If a session cookie is stolen, this cookie will never expire */ + maxAge: 86400000, overwrite: true, /** (boolean) can overwrite or not (default true) */ httpOnly: true, /** (boolean) httpOnly or not (default true) */ signed: true, /** (boolean) signed or not (default true) */ diff --git a/lib/context.js b/lib/context.js index 61650b0..fb38789 100644 --- a/lib/context.js +++ b/lib/context.js @@ -153,7 +153,7 @@ class ContextSession { valid(json) { if (!json) return false; - if (!json._expire || json._expire < Date.now()) { + if (json._expire && json._expire < Date.now()) { debug('expired session'); return false; } @@ -242,13 +242,18 @@ class ContextSession { const opts = this.opts; const key = opts.key; const externalKey = this.externalKey; - - const maxAge = opts.maxAge || ONE_DAY; - let json = this.session.toJSON(); // set expire for check - json._expire = maxAge + Date.now(); - json._maxAge = maxAge; + const maxAge = opts.maxAge ? opts.maxAge : ONE_DAY; + if (maxAge === 'session') { + // do not set _expire in json if maxAge is set to 'session' + // also delete maxAge from options + opts.maxAge = undefined; + } else { + // set expire for check + json._expire = maxAge + Date.now(); + json._maxAge = maxAge; + } // save to external store if (externalKey) { diff --git a/test/store.test.js b/test/store.test.js index 1e54227..85e588a 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -356,6 +356,60 @@ describe('Koa Session External Store', () => { }); describe('when maxAge present', () => { + describe('and set to be a session cookie', () => { + it('should not expire the session', done => { + const app = App({ maxAge: 'session' }); + + app.use(function* () { + if (this.method === 'POST') { + this.session.message = 'hi'; + this.body = 200; + return; + } + this.body = this.session.message; + }); + const server = app.listen(); + + request(server) + .post('/') + .expect('Set-Cookie', /koa:sess/) + .end((err, res) => { + if (err) return done(err); + const cookie = res.headers['set-cookie'].join(';'); + cookie.should.not.containEql('expires='); + request(server) + .get('/') + .set('cookie', cookie) + .expect('hi', done); + }); + }); + it('should use the default maxAge when improper string given', done => { + const app = App({ maxAge: 'not the right string' }); + + app.use(function* () { + if (this.method === 'POST') { + this.session.message = 'hi'; + this.body = 200; + return; + } + this.body = this.session.message; + }); + const server = app.listen(); + + request(server) + .post('/') + .expect('Set-Cookie', /koa:sess/) + .end((err, res) => { + if (err) return done(err); + const cookie = res.headers['set-cookie'].join(';'); + cookie.should.containEql('expires='); + request(server) + .get('/') + .set('cookie', cookie) + .expect('hi', done); + }); + }); + }); describe('and not expire', () => { it('should not expire the session', done => { const app = App({ maxAge: 100 }); From 6b61a6d1463b7e175c4ab73400a93afce4c93228 Mon Sep 17 00:00:00 2001 From: dead-horse Date: Thu, 1 Jun 2017 16:24:05 +0800 Subject: [PATCH 02/24] test: test on node 8 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 0c63626..d8cc0f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,6 @@ node_js: - '4' - '6' - '7' + - '8' script: 'npm run test-travis' after_script: 'npm install coveralls@2 && cat ./coverage/lcov.info | coveralls' From ae21f5212cd578439bb310f8082886ffaadb2905 Mon Sep 17 00:00:00 2001 From: dead-horse Date: Thu, 1 Jun 2017 16:30:13 +0800 Subject: [PATCH 03/24] Release 4.1.0 --- History.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index b33ced1..e408e50 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,9 @@ +4.1.0 / 2017-06-01 +================== + + * Create capability to create cookies that expire when browser is close… (#77) + 4.0.1 / 2017-03-01 ================== diff --git a/package.json b/package.json index 9691b3c..d54fd58 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware", "repository": "koajs/session", - "version": "4.0.1", + "version": "4.1.0", "keywords": [ "koa", "middleware", From dd2fa4ae4f3b301ba09b6512354300869d383e72 Mon Sep 17 00:00:00 2001 From: Shawn Date: Thu, 15 Jun 2017 12:02:54 +0800 Subject: [PATCH 04/24] feat: support options.ContextStore (#80) --- Readme.md | 2 + index.js | 18 +- lib/context.js | 4 +- package.json | 1 + test/context_store.js | 25 ++ test/contextstore.test.js | 640 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 684 insertions(+), 6 deletions(-) create mode 100644 test/context_store.js create mode 100644 test/contextstore.test.js diff --git a/Readme.md b/Readme.md index 337b65e..74bda5b 100644 --- a/Readme.md +++ b/Readme.md @@ -118,6 +118,8 @@ app.use(convert(session(app))); Once you passed `options.store`, session is strong dependent on your external store, you can't access session if your external store is down. **Use external session stores only if necessary, avoid use session as a cache, keep session lean and stored by cookie!** + If your session store requires data or utilities from context, `opts.ContextStore` is alse supported. `ContextStore` must be a class which claims three instance methods demonstrated above. `new ContextStore(ctx)` will be executed on every request. + ### Session#isNew Returns __true__ if the session is new. diff --git a/index.js b/index.js index 53ca820..04df5c4 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const debug = require('debug')('koa-session'); const ContextSession = require('./lib/context'); const util = require('./lib/util'); const assert = require('assert'); +const is = require('is-type-of'); const CONTEXT_SESSION = Symbol('context#contextSession'); const _CONTEXT_SESSION = Symbol('context#_contextSession'); @@ -78,10 +79,19 @@ function formatOpts(opts) { opts.decode = util.decode; } - if (opts.store) { - assert(typeof opts.store.get === 'function', 'store.get must be function'); - assert(typeof opts.store.set === 'function', 'store.set must be function'); - assert(typeof opts.store.destroy === 'function', 'store.destroy must be function'); + const store = opts.store; + if (store) { + assert(is.function(store.get), 'store.get must be function'); + assert(is.function(store.set), 'store.set must be function'); + assert(is.function(store.destroy), 'store.destroy must be function'); + } + + const ContextStore = opts.ContextStore; + if (ContextStore) { + assert(is.class(ContextStore), 'ContextStore must be a class'); + assert(is.function(ContextStore.prototype.get), 'ContextStore.prototype.get must be function'); + assert(is.function(ContextStore.prototype.set), 'ContextStore.prototype.set must be function'); + assert(is.function(ContextStore.prototype.destroy), 'ContextStore.prototype.destroy must be function'); } return opts; diff --git a/lib/context.js b/lib/context.js index fb38789..ecd78e6 100644 --- a/lib/context.js +++ b/lib/context.js @@ -16,7 +16,7 @@ class ContextSession { constructor(ctx, opts) { this.ctx = ctx; this.opts = Object.assign({}, opts); - this.store = this.opts.store; + this.store = this.opts.ContextStore ? new this.opts.ContextStore(ctx) : this.opts.store; } /** @@ -177,7 +177,7 @@ class ContextSession { create(val, externalKey) { debug('create session with val: %j externalKey: %s', val, externalKey); - if (this.opts.store) this.externalKey = externalKey || uid.sync(24); + if (this.store) this.externalKey = externalKey || uid.sync(24); this.session = new Session(this.ctx, val); } diff --git a/package.json b/package.json index d54fd58..4aba680 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "dependencies": { "crc": "^3.4.4", "debug": "^2.2.0", + "is-type-of": "^1.0.0", "uid-safe": "^2.1.3" }, "engines": { diff --git a/test/context_store.js b/test/context_store.js new file mode 100644 index 0000000..fb53af6 --- /dev/null +++ b/test/context_store.js @@ -0,0 +1,25 @@ +'use strict'; + +// this is a stupid nonsense example just to test + +const sessions = {}; + +class ContextStore { + constructor(ctx) { + this.ctx = ctx; + } + + * get(key) { + return sessions[key]; + } + + * set(key, value) { + sessions[key] = value; + } + + * destroy(key) { + sessions[key] = undefined; + } +} + +module.exports = ContextStore; diff --git a/test/contextstore.test.js b/test/contextstore.test.js new file mode 100644 index 0000000..1c9841a --- /dev/null +++ b/test/contextstore.test.js @@ -0,0 +1,640 @@ +'use strict'; + +const koa = require('koa'); +const request = require('supertest'); +const should = require('should'); +const mm = require('mm'); +const session = require('..'); +const ContextStore = require('./context_store'); + +describe('Koa Session External Context Store', () => { + let cookie; + + describe('when the session contains a ;', () => { + it('should still work', done => { + const app = App(); + + app.use(function* () { + if (this.method === 'POST') { + this.session.string = ';'; + this.status = 204; + } else { + this.body = this.session.string; + } + }); + + const server = app.listen(); + + request(server) + .post('/') + .expect(204, (err, res) => { + if (err) return done(err); + const cookie = res.headers['set-cookie']; + request(server) + .get('/') + .set('Cookie', cookie.join(';')) + .expect(';', done); + }); + }); + }); + + describe('new session', () => { + describe('when not accessed', () => { + it('should not Set-Cookie', done => { + const app = App(); + + app.use(function* () { + this.body = 'greetings'; + }); + + request(app.listen()) + .get('/') + .expect(200, (err, res) => { + if (err) return done(err); + res.header.should.not.have.property('set-cookie'); + done(); + }); + }); + }); + + describe('when accessed and not populated', () => { + it('should not Set-Cookie', done => { + const app = App(); + + app.use(function* () { + this.session; + this.body = 'greetings'; + }); + + request(app.listen()) + .get('/') + .expect(200, (err, res) => { + if (err) return done(err); + res.header.should.not.have.property('set-cookie'); + done(); + }); + }); + }); + + describe('when populated', () => { + it('should Set-Cookie', done => { + const app = App(); + + app.use(function* () { + this.session.message = 'hello'; + this.body = ''; + }); + + request(app.listen()) + .get('/') + .expect('Set-Cookie', /koa:sess/) + .expect(200, (err, res) => { + if (err) return done(err); + cookie = res.header['set-cookie'].join(';'); + done(); + }); + }); + + it('should not Set-Cookie', done => { + const app = App(); + + app.use(function* () { + this.body = this.session; + }); + + request(app.listen()) + .get('/') + .expect(200, (err, res) => { + if (err) return done(err); + res.header.should.not.have.property('set-cookie'); + done(); + }); + }); + }); + }); + + describe('saved session', () => { + describe('when not accessed', () => { + it('should not Set-Cookie', done => { + const app = App(); + + app.use(function* () { + this.body = 'aklsdjflasdjf'; + }); + + request(app.listen()) + .get('/') + .set('Cookie', cookie) + .expect(200, (err, res) => { + if (err) return done(err); + res.header.should.not.have.property('set-cookie'); + done(); + }); + }); + }); + + describe('when accessed but not changed', () => { + it('should be the same session', done => { + const app = App(); + + app.use(function* () { + this.session.message.should.equal('hello'); + this.body = 'aklsdjflasdjf'; + }); + + request(app.listen()) + .get('/') + .set('Cookie', cookie) + .expect(200, done); + }); + + it('should not Set-Cookie', done => { + const app = App(); + + app.use(function* () { + this.session.message.should.equal('hello'); + this.body = 'aklsdjflasdjf'; + }); + + request(app.listen()) + .get('/') + .set('Cookie', cookie) + .expect(200, (err, res) => { + if (err) return done(err); + res.header.should.not.have.property('set-cookie'); + done(); + }); + }); + }); + + describe('when accessed and changed', () => { + it('should Set-Cookie', done => { + const app = App(); + + app.use(function* () { + this.session.money = '$$$'; + this.body = 'aklsdjflasdjf'; + }); + + request(app.listen()) + .get('/') + .set('Cookie', cookie) + .expect('Set-Cookie', /koa:sess/) + .expect(200, done); + }); + }); + }); + + describe('when session is', () => { + describe('null', () => { + it('should expire the session', done => { + const app = App(); + + app.use(function* () { + this.session = null; + this.body = 'asdf'; + }); + + request(app.listen()) + .get('/') + .expect('Set-Cookie', /koa:sess/) + .expect(200, done); + }); + }); + + describe('an empty object', () => { + it('should not Set-Cookie', done => { + const app = App(); + + app.use(function* () { + this.session = {}; + this.body = 'asdf'; + }); + + request(app.listen()) + .get('/') + .expect(200, (err, res) => { + if (err) return done(err); + res.header.should.not.have.property('set-cookie'); + done(); + }); + }); + }); + + describe('an object', () => { + it('should create a session', done => { + const app = App(); + + app.use(function* () { + this.session = { message: 'hello' }; + this.body = 'asdf'; + }); + + request(app.listen()) + .get('/') + .expect('Set-Cookie', /koa:sess/) + .expect(200, done); + }); + }); + + describe('anything else', () => { + it('should throw', done => { + const app = App(); + + app.use(function* () { + this.session = 'asdf'; + }); + + request(app.listen()) + .get('/') + .expect(500, done); + }); + }); + }); + + describe('session', () => { + describe('.inspect()', () => { + it('should return session content', done => { + const app = App(); + + app.use(function* () { + this.session.foo = 'bar'; + this.body = this.session.inspect(); + }); + + request(app.listen()) + .get('/') + .expect('Set-Cookie', /koa:sess=.+;/) + .expect({ foo: 'bar' }) + .expect(200, done); + }); + }); + + describe('.length', () => { + it('should return session length', done => { + const app = App(); + + app.use(function* () { + this.session.foo = 'bar'; + this.body = String(this.session.length); + }); + + request(app.listen()) + .get('/') + .expect('Set-Cookie', /koa:sess=.+;/) + .expect('1') + .expect(200, done); + }); + }); + + describe('.populated', () => { + it('should return session populated', done => { + const app = App(); + + app.use(function* () { + this.session.foo = 'bar'; + this.body = String(this.session.populated); + }); + + request(app.listen()) + .get('/') + .expect('Set-Cookie', /koa:sess=.+;/) + .expect('true') + .expect(200, done); + }); + }); + + describe('.save()', () => { + it('should save session', done => { + const app = App(); + + app.use(function* () { + this.session.save(); + this.body = 'hello'; + }); + + request(app.listen()) + .get('/') + .expect('Set-Cookie', /koa:sess=.+;/) + .expect('hello') + .expect(200, done); + }); + }); + }); + + describe('when an error is thrown downstream and caught upstream', () => { + it('should still save the session', done => { + const app = koa(); + + app.keys = [ 'a', 'b' ]; + + app.use(function* (next) { + try { + yield next; + } catch (err) { + this.status = err.status; + this.body = err.message; + } + }); + + app.use(session({ ContextStore }, app)); + + app.use(function* (next) { + this.session.name = 'funny'; + yield next; + }); + + app.use(function* () { + this.throw(401); + }); + + request(app.listen()) + .get('/') + .expect('Set-Cookie', /koa:sess/) + .expect(401, done); + }); + }); + + describe('when maxAge present', () => { + describe('and set to be a session cookie', () => { + it('should not expire the session', done => { + const app = App({ maxAge: 'session' }); + + app.use(function* () { + if (this.method === 'POST') { + this.session.message = 'hi'; + this.body = 200; + return; + } + this.body = this.session.message; + }); + const server = app.listen(); + + request(server) + .post('/') + .expect('Set-Cookie', /koa:sess/) + .end((err, res) => { + if (err) return done(err); + const cookie = res.headers['set-cookie'].join(';'); + cookie.should.not.containEql('expires='); + request(server) + .get('/') + .set('cookie', cookie) + .expect('hi', done); + }); + }); + it('should use the default maxAge when improper string given', done => { + const app = App({ maxAge: 'not the right string' }); + + app.use(function* () { + if (this.method === 'POST') { + this.session.message = 'hi'; + this.body = 200; + return; + } + this.body = this.session.message; + }); + const server = app.listen(); + + request(server) + .post('/') + .expect('Set-Cookie', /koa:sess/) + .end((err, res) => { + if (err) return done(err); + const cookie = res.headers['set-cookie'].join(';'); + cookie.should.containEql('expires='); + request(server) + .get('/') + .set('cookie', cookie) + .expect('hi', done); + }); + }); + }); + describe('and not expire', () => { + it('should not expire the session', done => { + const app = App({ maxAge: 100 }); + + app.use(function* () { + if (this.method === 'POST') { + this.session.message = 'hi'; + this.body = 200; + return; + } + this.body = this.session.message; + }); + + const server = app.listen(); + + request(server) + .post('/') + .expect('Set-Cookie', /koa:sess/) + .end((err, res) => { + if (err) return done(err); + const cookie = res.headers['set-cookie'].join(';'); + + request(server) + .get('/') + .set('cookie', cookie) + .expect('hi', done); + }); + }); + }); + + describe('and expired', () => { + it('should expire the sess', done => { + const app = App({ maxAge: 100 }); + + app.use(function* () { + if (this.method === 'POST') { + this.session.message = 'hi'; + this.status = 200; + return; + } + + this.body = this.session.message || ''; + }); + + const server = app.listen(); + + request(server) + .post('/') + .expect('Set-Cookie', /koa:sess/) + .end((err, res) => { + if (err) return done(err); + const cookie = res.headers['set-cookie'].join(';'); + + setTimeout(() => { + request(server) + .get('/') + .set('cookie', cookie) + .expect('', done); + }, 200); + }); + }); + }); + }); + + describe('ctx.session.maxAge', () => { + it('should return opt.maxAge', done => { + const app = App({ maxAge: 100 }); + + app.use(function* () { + this.body = this.session.maxAge; + }); + + request(app.listen()) + .get('/') + .expect('100', done); + }); + }); + + describe('ctx.session.maxAge=', () => { + it('should set sessionOptions.maxAge', done => { + const app = App(); + + app.use(function* () { + this.session.foo = 'bar'; + this.session.maxAge = 100; + this.body = this.session.foo; + }); + + request(app.listen()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200, done); + }); + + it('should save even session not change', done => { + const app = App(); + + app.use(function* () { + this.session.maxAge = 100; + this.body = this.session; + }); + + request(app.listen()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200, done); + }); + + it('should save when create session only with maxAge', done => { + const app = App(); + + app.use(function* () { + this.session = { maxAge: 100 }; + this.body = this.session; + }); + + request(app.listen()) + .get('/') + .expect('Set-Cookie', /expires=/) + .expect(200, done); + }); + }); + + describe('when store return empty', () => { + it('should create new Session', done => { + const app = App({ signed: false }); + + app.use(function* () { + this.body = String(this.session.isNew); + }); + + request(app.listen()) + .get('/') + .set('cookie', 'koa:sess=invalid-key') + .expect('true') + .expect(200, done); + }); + }); + + describe('when valid and beforeSave set', () => { + it('should ignore session when uid changed', done => { + const app = koa(); + + app.keys = [ 'a', 'b' ]; + app.use(session({ + valid(ctx, sess) { + return ctx.cookies.get('uid') === sess.uid; + }, + beforeSave(ctx, sess) { + sess.uid = ctx.cookies.get('uid'); + }, + ContextStore, + }, app)); + app.use(function* () { + if (!this.session.foo) { + this.session.foo = Date.now() + '|uid:' + this.cookies.get('uid'); + } + + this.body = { + foo: this.session.foo, + uid: this.cookies.get('uid'), + }; + }); + + request(app.callback()) + .get('/') + .set('Cookie', 'uid=123') + .expect(200, (err, res) => { + should.not.exist(err); + const data = res.body; + const cookies = res.headers['set-cookie'].join(';'); + cookies.should.containEql('koa:sess='); + + request(app.callback()) + .get('/') + .set('Cookie', cookies + ';uid=123') + .expect(200) + .expect(data, err => { + should.not.exist(err); + + // should ignore uid:123 session and create a new session for uid:456 + request(app.callback()) + .get('/') + .set('Cookie', cookies + ';uid=456') + .expect(200, (err, res) => { + should.not.exist(err); + res.body.uid.should.equal('456'); + res.body.foo.should.not.equal(data.foo); + done(); + }); + }); + }); + }); + }); + + describe('ctx.session', () => { + after(mm.restore); + + it('should be mocked', done => { + const app = App(); + + app.use(function* () { + this.body = this.session; + }); + + mm(app.context, 'session', { + foo: 'bar', + }); + + request(app.listen()) + .get('/') + .expect({ + foo: 'bar', + }) + .expect(200, done); + }); + }); + +}); + +function App(options) { + const app = koa(); + app.keys = [ 'a', 'b' ]; + options = options || {}; + options.ContextStore = ContextStore; + app.use(session(options, app)); + return app; +} From 0d94eb4e758114696a0a9596436dd4ca53f31ed4 Mon Sep 17 00:00:00 2001 From: dead-horse Date: Thu, 15 Jun 2017 12:03:47 +0800 Subject: [PATCH 05/24] Release 4.2.0 --- History.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index e408e50..4c44d74 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,9 @@ +4.2.0 / 2017-06-15 +================== + + * feat: support options.ContextStore (#80) + 4.1.0 / 2017-06-01 ================== diff --git a/package.json b/package.json index 4aba680..862d5ab 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware", "repository": "koajs/session", - "version": "4.1.0", + "version": "4.2.0", "keywords": [ "koa", "middleware", From 5ba45dd098214f3f6aed254c0445abee7f78cefe Mon Sep 17 00:00:00 2001 From: dead-horse Date: Thu, 15 Jun 2017 12:04:36 +0800 Subject: [PATCH 06/24] chore: add dist-tag --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 862d5ab..cd36bdb 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,9 @@ "engines": { "node": ">=4" }, + "publishConfig": { + "tag": "v4" + }, "scripts": { "test": "npm run lint && NODE_ENV=test mocha --require should --reporter spec test/*.test.js", "test-cov": "NODE_ENV=test node ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --require should test/*.test.js", From 558ecf35b8689af4ee84d4704f6aae63f4bcbaed Mon Sep 17 00:00:00 2001 From: Yiyu He Date: Sat, 17 Jun 2017 00:17:08 +0800 Subject: [PATCH 07/24] feat: support rolling (#82) * feat: support rolling * f * feat: pass options to store --- Readme.md | 6 ++++-- lib/context.js | 17 +++++++++++------ test/cookie.test.js | 40 ++++++++++++++++++++++++++++++++++++++++ test/store.test.js | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 8 deletions(-) diff --git a/Readme.md b/Readme.md index 74bda5b..3c2c043 100644 --- a/Readme.md +++ b/Readme.md @@ -54,7 +54,9 @@ var CONFIG = { overwrite: true, /** (boolean) can overwrite or not (default true) */ httpOnly: true, /** (boolean) httpOnly or not (default true) */ signed: true, /** (boolean) signed or not (default true) */ + rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. default is false **/ }; + app.use(session(CONFIG, app)); // or if you prefer all default config, just use => app.use(session(app)); @@ -111,8 +113,8 @@ app.use(convert(session(app))); You can store the session content in external stores(redis, mongodb or other DBs) by pass `options.store` with three methods(need to be generator function or async function): - - `get(key)`: get session object by key - - `set(key, sess, maxAge)`: set session object for key, with a `maxAge` (in ms) + - `get(key, maxAge, { rolling })`: get session object by key + - `set(key, sess, maxAge, { rolling, changed })`: set session object for key, with a `maxAge` (in ms) - `destroy(key)`: destroy session for key diff --git a/lib/context.js b/lib/context.js index ecd78e6..4f5f93e 100644 --- a/lib/context.js +++ b/lib/context.js @@ -79,7 +79,7 @@ class ContextSession { return; } - const json = yield this.store.get(externalKey); + const json = yield this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling }); if (!this.valid(json)) { // create a new `externalKey` this.create(); @@ -203,19 +203,21 @@ class ContextSession { } // force save session when `session._requireSave` set + let changed = true; if (!session._requireSave) { const json = session.toJSON(); // do nothing if new and not populated if (!prevHash && !Object.keys(json).length) return; - // do nothing if not changed - if (prevHash === util.hash(json)) return; + changed = prevHash !== util.hash(json); + // do nothing if not changed and not in rolling mode + if (!this.opts.rolling && !changed) return; } if (typeof opts.beforeSave === 'function') { debug('before save'); opts.beforeSave(ctx, session); } - yield this.save(); + yield this.save(changed); } /** @@ -238,7 +240,7 @@ class ContextSession { * @api private */ - * save() { + * save(changed) { const opts = this.opts; const key = opts.key; const externalKey = this.externalKey; @@ -258,7 +260,10 @@ class ContextSession { // save to external store if (externalKey) { debug('save %j to external key %s', json, externalKey); - yield this.store.set(externalKey, json, maxAge); + yield this.store.set(externalKey, json, maxAge, { + changed, + rolling: opts.rolling, + }); this.ctx.cookies.set(key, externalKey, opts); return; } diff --git a/test/cookie.test.js b/test/cookie.test.js index 56cfcf2..d306f26 100644 --- a/test/cookie.test.js +++ b/test/cookie.test.js @@ -755,6 +755,46 @@ describe('Koa Session Cookie', () => { }); }); }); + + describe('when rolling set to true', () => { + let app; + before(() => { + app = App({ rolling: true }); + + app.use(function* () { + console.log(this.path); + if (this.path === '/set') this.session = { foo: 'bar' }; + this.body = this.session; + }); + }); + + it('should not send set-cookie when session not exists', () => { + return request(app.callback()) + .get('/') + .expect({}) + .expect(res => { + should.not.exist(res.headers['set-cookie']); + }); + }); + + it('should send set-cookie when session exists and not change', done => { + request(app.callback()) + .get('/set') + .expect({ foo: 'bar' }) + .end((err, res) => { + should.not.exist(err); + res.headers['set-cookie'].should.have.length(2); + const cookie = res.headers['set-cookie'].join(';'); + request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect(res => { + res.headers['set-cookie'].should.have.length(2); + }) + .expect({ foo: 'bar' }, done); + }); + }); + }); }); function App(options) { diff --git a/test/store.test.js b/test/store.test.js index 85e588a..7d58df5 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -628,6 +628,44 @@ describe('Koa Session External Store', () => { }); }); + describe('when rolling set to true', () => { + let app; + before(() => { + app = App({ rolling: true }); + + app.use(function* () { + if (this.path === '/set') this.session = { foo: 'bar' }; + this.body = this.session; + }); + }); + + it('should not send set-cookie when session not exists', () => { + return request(app.callback()) + .get('/') + .expect({}) + .expect(res => { + should.not.exist(res.headers['set-cookie']); + }); + }); + + it('should send set-cookie when session exists and not change', done => { + request(app.callback()) + .get('/set') + .expect({ foo: 'bar' }) + .end((err, res) => { + should.not.exist(err); + res.headers['set-cookie'].should.have.length(2); + const cookie = res.headers['set-cookie'].join(';'); + request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect(res => { + res.headers['set-cookie'].should.have.length(2); + }) + .expect({ foo: 'bar' }, done); + }); + }); + }); }); function App(options) { From 7effcf2cd2569e2f9b4c6234480593ba75ee28f4 Mon Sep 17 00:00:00 2001 From: dead-horse Date: Sat, 17 Jun 2017 00:20:19 +0800 Subject: [PATCH 08/24] Release 4.3.0 --- History.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 4c44d74..8a0d5fb 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,9 @@ +4.3.0 / 2017-06-17 +================== + + * feat: support rolling (#82) + 4.2.0 / 2017-06-15 ================== diff --git a/package.json b/package.json index cd36bdb..9b0cc38 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware", "repository": "koajs/session", - "version": "4.2.0", + "version": "4.3.0", "keywords": [ "koa", "middleware", From e9e4d5eefff170e719563b1302082deddafcf1b5 Mon Sep 17 00:00:00 2001 From: Shawn Date: Mon, 3 Jul 2017 16:50:00 +0800 Subject: [PATCH 09/24] feat: opts.genid (#86) --- Readme.md | 2 ++ lib/context.js | 2 +- test/contextstore.test.js | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 3c2c043..1943e27 100644 --- a/Readme.md +++ b/Readme.md @@ -120,6 +120,8 @@ app.use(convert(session(app))); Once you passed `options.store`, session is strong dependent on your external store, you can't access session if your external store is down. **Use external session stores only if necessary, avoid use session as a cache, keep session lean and stored by cookie!** + The way of generating external session id is controlled by the `options.genid`, which defaults to `uid.sync(24)`. + If your session store requires data or utilities from context, `opts.ContextStore` is alse supported. `ContextStore` must be a class which claims three instance methods demonstrated above. `new ContextStore(ctx)` will be executed on every request. ### Session#isNew diff --git a/lib/context.js b/lib/context.js index 4f5f93e..c2e8dee 100644 --- a/lib/context.js +++ b/lib/context.js @@ -177,7 +177,7 @@ class ContextSession { create(val, externalKey) { debug('create session with val: %j externalKey: %s', val, externalKey); - if (this.store) this.externalKey = externalKey || uid.sync(24); + if (this.store) this.externalKey = externalKey || this.opts.genid && this.opts.genid(this.ctx) || uid.sync(24); this.session = new Session(this.ctx, val); } diff --git a/test/contextstore.test.js b/test/contextstore.test.js index 1c9841a..6a8d383 100644 --- a/test/contextstore.test.js +++ b/test/contextstore.test.js @@ -91,6 +91,7 @@ describe('Koa Session External Context Store', () => { .expect(200, (err, res) => { if (err) return done(err); cookie = res.header['set-cookie'].join(';'); + cookie.indexOf('_suffix').should.greaterThan(1); done(); }); }); @@ -635,6 +636,9 @@ function App(options) { app.keys = [ 'a', 'b' ]; options = options || {}; options.ContextStore = ContextStore; + options.genid = () => { + return Date.now() + '_suffix'; + }; app.use(session(options, app)); return app; } From 24f1def612a48be8300d4686b7abe560b261b877 Mon Sep 17 00:00:00 2001 From: dead-horse Date: Mon, 3 Jul 2017 16:51:14 +0800 Subject: [PATCH 10/24] Release 4.4.0 --- History.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 8a0d5fb..e8ba1e8 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,9 @@ +4.4.0 / 2017-07-03 +================== + + * feat: opts.genid (#86) + 4.3.0 / 2017-06-17 ================== diff --git a/package.json b/package.json index 9b0cc38..08845b7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware", "repository": "koajs/session", - "version": "4.3.0", + "version": "4.4.0", "keywords": [ "koa", "middleware", From b537deaeb2db352bdcab3be2c8b7578760fc69da Mon Sep 17 00:00:00 2001 From: Yiyu He Date: Fri, 4 Aug 2017 10:53:34 +0800 Subject: [PATCH 11/24] feat: support options.prefix for external store (#92) --- Readme.md | 2 ++ index.js | 6 ++++++ lib/context.js | 3 +-- test/store.test.js | 29 +++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 1943e27..4592549 100644 --- a/Readme.md +++ b/Readme.md @@ -122,6 +122,8 @@ app.use(convert(session(app))); The way of generating external session id is controlled by the `options.genid`, which defaults to `uid.sync(24)`. + If you want to add prefix for all external session id, you can use `options.prefix`, it will not work if `options.genid` present. + If your session store requires data or utilities from context, `opts.ContextStore` is alse supported. `ContextStore` must be a class which claims three instance methods demonstrated above. `new ContextStore(ctx)` will be executed on every request. ### Session#isNew diff --git a/index.js b/index.js index 04df5c4..fe767ba 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const debug = require('debug')('koa-session'); const ContextSession = require('./lib/context'); const util = require('./lib/util'); const assert = require('assert'); +const uid = require('uid-safe'); const is = require('is-type-of'); const CONTEXT_SESSION = Symbol('context#contextSession'); @@ -94,6 +95,11 @@ function formatOpts(opts) { assert(is.function(ContextStore.prototype.destroy), 'ContextStore.prototype.destroy must be function'); } + if (!opts.genid) { + if (opts.prefix) opts.genid = () => opts.prefix + uid.sync(24); + else opts.genid = () => uid.sync(24); + } + return opts; } diff --git a/lib/context.js b/lib/context.js index c2e8dee..f2df3a5 100644 --- a/lib/context.js +++ b/lib/context.js @@ -2,7 +2,6 @@ const debug = require('debug')('koa-session:context'); const Session = require('./session'); -const uid = require('uid-safe'); const util = require('./util'); const ONE_DAY = 24 * 60 * 60 * 1000; @@ -177,7 +176,7 @@ class ContextSession { create(val, externalKey) { debug('create session with val: %j externalKey: %s', val, externalKey); - if (this.store) this.externalKey = externalKey || this.opts.genid && this.opts.genid(this.ctx) || uid.sync(24); + if (this.store) this.externalKey = externalKey || this.opts.genid(); this.session = new Session(this.ctx, val); } diff --git a/test/store.test.js b/test/store.test.js index 7d58df5..7b4d7c4 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -666,6 +666,35 @@ describe('Koa Session External Store', () => { }); }); }); + + describe('when prefix present', () => { + it('should still work', done => { + const app = App({ prefix: 'sess:' }); + + app.use(function* () { + if (this.method === 'POST') { + this.session.string = ';'; + this.status = 204; + } else { + this.body = this.session.string; + } + }); + + const server = app.listen(); + + request(server) + .post('/') + .expect(204, (err, res) => { + if (err) return done(err); + const cookie = res.headers['set-cookie']; + cookie.join().should.match(/koa:sess=sess:/); + request(server) + .get('/') + .set('Cookie', cookie.join(';')) + .expect(';', done); + }); + }); + }); }); function App(options) { From 001926f8bdd4f79e306e9a80d926bf040ac8377e Mon Sep 17 00:00:00 2001 From: dead-horse Date: Fri, 4 Aug 2017 10:54:14 +0800 Subject: [PATCH 12/24] Release 4.5.0 --- History.md | 6 ++++++ package.json | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/History.md b/History.md index e8ba1e8..85daf25 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,10 @@ +4.5.0 / 2017-08-04 +================== + +**features** + * [[`b537dea`](http://github.com/koajs/session/commit/b537deaeb2db352bdcab3be2c8b7578760fc69da)] - feat: support options.prefix for external store (#92) (Yiyu He <>) + 4.4.0 / 2017-07-03 ================== diff --git a/package.json b/package.json index 08845b7..5ced8f1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware", "repository": "koajs/session", - "version": "4.4.0", + "version": "4.5.0", "keywords": [ "koa", "middleware", @@ -42,4 +42,4 @@ "test-travis": "npm run lint && NODE_ENV=test node ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- --require should test/*.test.js", "lint": "eslint lib test index.js" } -} +} \ No newline at end of file From c8298a3f60aabc3067f0a2db0409df05f1ed71a1 Mon Sep 17 00:00:00 2001 From: Yiyu He Date: Tue, 9 Jan 2018 17:34:35 +0800 Subject: [PATCH 13/24] feat: emit events when session invalid (#109) session key add Date.now() --- Readme.md | 14 ++++++++++++-- index.js | 4 ++-- lib/context.js | 19 +++++++++++++------ package.json | 1 + test/store.test.js | 20 ++++++++++++++++++++ 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Readme.md b/Readme.md index 4592549..68bd8c8 100644 --- a/Readme.md +++ b/Readme.md @@ -26,7 +26,9 @@ [download-image]: https://img.shields.io/npm/dm/koa-session.svg?style=flat-square [download-url]: https://npmjs.org/package/koa-session - Simple cookie-based session middleware for Koa. + Simple session middleware for Koa. Defaults to cookie-based sessions and supports external stores. + + *Requires Node 7.6 or greater for async/await support* ## Installation @@ -120,12 +122,20 @@ app.use(convert(session(app))); Once you passed `options.store`, session is strong dependent on your external store, you can't access session if your external store is down. **Use external session stores only if necessary, avoid use session as a cache, keep session lean and stored by cookie!** - The way of generating external session id is controlled by the `options.genid`, which defaults to `uid.sync(24)`. + The way of generating external session id is controlled by the `options.genid`, which defaults to `Date.now() + '-' + uid.sync(24)`. If you want to add prefix for all external session id, you can use `options.prefix`, it will not work if `options.genid` present. If your session store requires data or utilities from context, `opts.ContextStore` is alse supported. `ContextStore` must be a class which claims three instance methods demonstrated above. `new ContextStore(ctx)` will be executed on every request. +### Events + +`koa-session` will emit event on `app` when session expired or invalid: + +- `session:missed`: can't get session value from external store. +- `session:invalid`: session value is invalid. +- `session:expired`: session value is expired. + ### Session#isNew Returns __true__ if the session is new. diff --git a/index.js b/index.js index fe767ba..92dcc81 100644 --- a/index.js +++ b/index.js @@ -96,8 +96,8 @@ function formatOpts(opts) { } if (!opts.genid) { - if (opts.prefix) opts.genid = () => opts.prefix + uid.sync(24); - else opts.genid = () => uid.sync(24); + if (opts.prefix) opts.genid = () => `${opts.prefix}${Date.now()}-${uid.sync(24)}`; + else opts.genid = () => `${Date.now()}-${uid.sync(24)}`; } return opts; diff --git a/lib/context.js b/lib/context.js index f2df3a5..0803184 100644 --- a/lib/context.js +++ b/lib/context.js @@ -14,6 +14,7 @@ class ContextSession { constructor(ctx, opts) { this.ctx = ctx; + this.app = ctx.app; this.opts = Object.assign({}, opts); this.store = this.opts.ContextStore ? new this.opts.ContextStore(ctx) : this.opts.store; } @@ -79,7 +80,7 @@ class ContextSession { } const json = yield this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling }); - if (!this.valid(json)) { + if (!this.valid(json, externalKey)) { // create a new `externalKey` this.create(); return; @@ -144,23 +145,29 @@ class ContextSession { /** * verify session(expired or ) - * @param {Object} json session object + * @param {Object} value session object + * @param {Object} key session externalKey(optional) * @return {Boolean} valid * @api private */ - valid(json) { - if (!json) return false; + valid(value, key) { + if (!value) { + this.app.emit('session:missed', { key, value }); + return false; + } - if (json._expire && json._expire < Date.now()) { + if (value._expire && value._expire < Date.now()) { debug('expired session'); + this.app.emit('session:expired', { key, value }); return false; } const valid = this.opts.valid; - if (typeof valid === 'function' && !valid(this.ctx, json)) { + if (typeof valid === 'function' && !valid(this.ctx, value)) { // valid session value fail, ignore this session debug('invalid session'); + this.app.emit('session:invalid', { key, value }); return false; } return true; diff --git a/package.json b/package.json index 5ced8f1..0b52ad7 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "crc": "^3.4.4", "debug": "^2.2.0", "is-type-of": "^1.0.0", + "pedding": "^1.1.0", "uid-safe": "^2.1.3" }, "engines": { diff --git a/test/store.test.js b/test/store.test.js index 7b4d7c4..3a4f209 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -6,6 +6,8 @@ const should = require('should'); const mm = require('mm'); const session = require('..'); const store = require('./store'); +const pedding = require('pedding'); +const assert = require('assert'); describe('Koa Session External Store', () => { let cookie; @@ -442,6 +444,7 @@ describe('Koa Session External Store', () => { describe('and expired', () => { it('should expire the sess', done => { + done = pedding(done, 2); const app = App({ maxAge: 100 }); app.use(function* () { @@ -453,6 +456,11 @@ describe('Koa Session External Store', () => { this.body = this.session.message || ''; }); + app.on('session:expired', args => { + assert(args.key.match(/^\d+-/)); + assert(args.value); + done(); + }); const server = app.listen(); @@ -535,12 +543,18 @@ describe('Koa Session External Store', () => { describe('when store return empty', () => { it('should create new Session', done => { + done = pedding(done, 2); const app = App({ signed: false }); app.use(function* () { this.body = String(this.session.isNew); }); + app.on('session:missed', args => { + assert(args.key === 'invalid-key'); + done(); + }); + request(app.listen()) .get('/') .set('cookie', 'koa:sess=invalid-key') @@ -551,6 +565,7 @@ describe('Koa Session External Store', () => { describe('when valid and beforeSave set', () => { it('should ignore session when uid changed', done => { + done = pedding(done, 2); const app = koa(); app.keys = [ 'a', 'b' ]; @@ -573,6 +588,11 @@ describe('Koa Session External Store', () => { uid: this.cookies.get('uid'), }; }); + app.on('session:invalid', args => { + assert(args.key); + assert(args.value); + done(); + }); request(app.callback()) .get('/') From 1d68fbe68f65600fc09f5272839d2a85006bf75b Mon Sep 17 00:00:00 2001 From: dead-horse Date: Tue, 9 Jan 2018 17:34:57 +0800 Subject: [PATCH 14/24] Release 4.6.0 --- History.md | 6 ++++++ package.json | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/History.md b/History.md index 85daf25..fde9fa7 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,10 @@ +4.6.0 / 2018-01-09 +================== + +**features** + * [[`c8298a3`](http://github.com/koajs/session/commit/c8298a3f60aabc3067f0a2db0409df05f1ed71a1)] - feat: emit events when session invalid (#109) (Yiyu He <>) + 4.5.0 / 2017-08-04 ================== diff --git a/package.json b/package.json index 0b52ad7..a3c4742 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware", "repository": "koajs/session", - "version": "4.5.0", + "version": "4.6.0", "keywords": [ "koa", "middleware", @@ -43,4 +43,4 @@ "test-travis": "npm run lint && NODE_ENV=test node ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- --require should test/*.test.js", "lint": "eslint lib test index.js" } -} \ No newline at end of file +} From f8b18228c90a6a0d7b8d91c560b9675bafd5a552 Mon Sep 17 00:00:00 2001 From: dead-horse Date: Tue, 9 Jan 2018 17:38:25 +0800 Subject: [PATCH 15/24] feat: emit event expose ctx --- lib/context.js | 9 +++++---- test/store.test.js | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/context.js b/lib/context.js index 0803184..9fdfcc2 100644 --- a/lib/context.js +++ b/lib/context.js @@ -152,22 +152,23 @@ class ContextSession { */ valid(value, key) { + const ctx = this.ctx; if (!value) { - this.app.emit('session:missed', { key, value }); + this.app.emit('session:missed', { key, value, ctx }); return false; } if (value._expire && value._expire < Date.now()) { debug('expired session'); - this.app.emit('session:expired', { key, value }); + this.app.emit('session:expired', { key, value, ctx }); return false; } const valid = this.opts.valid; - if (typeof valid === 'function' && !valid(this.ctx, value)) { + if (typeof valid === 'function' && !valid(ctx, value)) { // valid session value fail, ignore this session debug('invalid session'); - this.app.emit('session:invalid', { key, value }); + this.app.emit('session:invalid', { key, value, ctx }); return false; } return true; diff --git a/test/store.test.js b/test/store.test.js index 3a4f209..02b9f12 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -459,6 +459,7 @@ describe('Koa Session External Store', () => { app.on('session:expired', args => { assert(args.key.match(/^\d+-/)); assert(args.value); + assert(args.ctx); done(); }); @@ -552,6 +553,7 @@ describe('Koa Session External Store', () => { app.on('session:missed', args => { assert(args.key === 'invalid-key'); + assert(args.ctx); done(); }); @@ -591,6 +593,7 @@ describe('Koa Session External Store', () => { app.on('session:invalid', args => { assert(args.key); assert(args.value); + assert(args.ctx); done(); }); From 948ab101927069f84bf8f871409ff4edba32dfe1 Mon Sep 17 00:00:00 2001 From: dead-horse Date: Tue, 9 Jan 2018 17:38:36 +0800 Subject: [PATCH 16/24] Release 4.7.0 --- History.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index fde9fa7..1cf9b42 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,10 @@ +4.7.0 / 2018-01-09 +================== + +**features** + * [[`f8b1822`](http://github.com/koajs/session/commit/f8b18228c90a6a0d7b8d91c560b9675bafd5a552)] - feat: emit event expose ctx (dead-horse <>) + 4.6.0 / 2018-01-09 ================== diff --git a/package.json b/package.json index a3c4742..815f9dd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware", "repository": "koajs/session", - "version": "4.6.0", + "version": "4.7.0", "keywords": [ "koa", "middleware", From 5683102687c48ba11521d8b249126de13e7c0d8b Mon Sep 17 00:00:00 2001 From: dead-horse Date: Thu, 11 Jan 2018 23:43:55 +0800 Subject: [PATCH 17/24] fix: emit event in next tick --- lib/context.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/context.js b/lib/context.js index 9fdfcc2..7186670 100644 --- a/lib/context.js +++ b/lib/context.js @@ -154,13 +154,13 @@ class ContextSession { valid(value, key) { const ctx = this.ctx; if (!value) { - this.app.emit('session:missed', { key, value, ctx }); + this.emit('missed', { key, value, ctx }); return false; } if (value._expire && value._expire < Date.now()) { debug('expired session'); - this.app.emit('session:expired', { key, value, ctx }); + this.emit('expired', { key, value, ctx }); return false; } @@ -168,12 +168,23 @@ class ContextSession { if (typeof valid === 'function' && !valid(ctx, value)) { // valid session value fail, ignore this session debug('invalid session'); - this.app.emit('session:invalid', { key, value, ctx }); + this.emit('invalid', { key, value, ctx }); return false; } return true; } + /** + * @param {String} event event name + * @param {Object} data event data + * @api private + */ + emit(event, data) { + setImmediate(() => { + this.app.emit(`session:${event}`, data); + }); + } + /** * create a new session and attach to ctx.sess * From 0bf7cfaf4b21b08c4d121a7e50d60e9962a81482 Mon Sep 17 00:00:00 2001 From: dead-horse Date: Thu, 11 Jan 2018 23:44:44 +0800 Subject: [PATCH 18/24] Release 4.7.1 --- History.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 1cf9b42..c685320 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,10 @@ +4.7.1 / 2018-01-11 +================== + +**fixes** + * [[`5683102`](http://github.com/koajs/session/commit/5683102687c48ba11521d8b249126de13e7c0d8b)] - fix: emit event in next tick (dead-horse <>) + 4.7.0 / 2018-01-09 ================== diff --git a/package.json b/package.json index 815f9dd..2b24c24 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware", "repository": "koajs/session", - "version": "4.7.0", + "version": "4.7.1", "keywords": [ "koa", "middleware", From ca6b32906678b3cf6168c4afac250b2e68fd17c8 Mon Sep 17 00:00:00 2001 From: Yiyu He Date: Wed, 17 Jan 2018 15:35:29 +0800 Subject: [PATCH 19/24] feat: support opts.renew (#111) --- Readme.md | 3 ++- lib/context.js | 44 +++++++++++++++++++++++++++++---------- package.json | 3 ++- test/store.test.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 13 deletions(-) diff --git a/Readme.md b/Readme.md index 68bd8c8..aed40f2 100644 --- a/Readme.md +++ b/Readme.md @@ -56,7 +56,8 @@ var CONFIG = { overwrite: true, /** (boolean) can overwrite or not (default true) */ httpOnly: true, /** (boolean) httpOnly or not (default true) */ signed: true, /** (boolean) signed or not (default true) */ - rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. default is false **/ + rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. (default is false) */ + renew: false, /** (boolean) renew session when session is nearly expired, so we can always keep user logged in. (default is false)*/ }; app.use(session(CONFIG, app)); diff --git a/lib/context.js b/lib/context.js index 7186670..5de4553 100644 --- a/lib/context.js +++ b/lib/context.js @@ -207,7 +207,6 @@ class ContextSession { * commit() { const session = this.session; - const prevHash = this.prevHash; const opts = this.opts; const ctx = this.ctx; @@ -220,24 +219,47 @@ class ContextSession { return; } - // force save session when `session._requireSave` set - let changed = true; - if (!session._requireSave) { - const json = session.toJSON(); - // do nothing if new and not populated - if (!prevHash && !Object.keys(json).length) return; - changed = prevHash !== util.hash(json); - // do nothing if not changed and not in rolling mode - if (!this.opts.rolling && !changed) return; - } + const reason = this._shouldSaveSession(); + debug('should save session: %s', reason); + if (!reason) return; if (typeof opts.beforeSave === 'function') { debug('before save'); opts.beforeSave(ctx, session); } + const changed = reason === 'changed'; yield this.save(changed); } + _shouldSaveSession() { + const prevHash = this.prevHash; + const session = this.session; + + // force save session when `session._requireSave` set + if (session._requireSave) return 'force'; + + // do nothing if new and not populated + const json = session.toJSON(); + if (!prevHash && !Object.keys(json).length) return ''; + + // save if session changed + const changed = prevHash !== util.hash(json); + if (changed) return 'changed'; + + // save if opts.rolling set + if (this.opts.rolling) return 'rolling'; + + // save if opts.renew and session will expired + if (this.opts.renew) { + const expire = session._expire; + // renew when session will expired in maxAge / 2 + if (!expire || expire - Date.now() > session.maxAge / 2) return ''; + return 'renew'; + } + + return ''; + } + /** * remove session * @api private diff --git a/package.json b/package.json index 2b24c24..0b1e112 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "istanbul": "0", "koa": "1", "mm": "^2.1.0", - "mocha": "3 ", + "mocha": "3", + "mz-modules": "^2.0.0", "should": "8", "supertest": "2" }, diff --git a/test/store.test.js b/test/store.test.js index 02b9f12..13c788d 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -8,6 +8,7 @@ const session = require('..'); const store = require('./store'); const pedding = require('pedding'); const assert = require('assert'); +const sleep = require('mz-modules/sleep'); describe('Koa Session External Store', () => { let cookie; @@ -718,6 +719,57 @@ describe('Koa Session External Store', () => { }); }); }); + + describe('when renew set to true', () => { + let app; + before(() => { + app = App({ renew: true, maxAge: 2000 }); + + app.use(function* () { + if (this.path === '/set') this.session = { foo: 'bar' }; + this.body = this.session; + }); + }); + + it('should not send set-cookie when session not exists', () => { + return request(app.callback()) + .get('/') + .expect({}) + .expect(res => { + should.not.exist(res.headers['set-cookie']); + }); + }); + + it('should send set-cookie when session near expire and not change', function* () { + let res = yield request(app.callback()) + .get('/set') + .expect({ foo: 'bar' }); + + res.headers['set-cookie'].should.have.length(2); + const cookie = res.headers['set-cookie'].join(';'); + yield sleep(1200); + res = yield request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect({ foo: 'bar' }); + res.headers['set-cookie'].should.have.length(2); + }); + + it('should not send set-cookie when session not near expire and not change', function* () { + let res = yield request(app.callback()) + .get('/set') + .expect({ foo: 'bar' }); + + res.headers['set-cookie'].should.have.length(2); + const cookie = res.headers['set-cookie'].join(';'); + yield sleep(500); + res = yield request(app.callback()) + .get('/') + .set('cookie', cookie) + .expect({ foo: 'bar' }); + should.not.exist(res.headers['set-cookie']); + }); + }); }); function App(options) { From 5c2fc72cab47450da2c9ae8fedba8657d0c264ee Mon Sep 17 00:00:00 2001 From: dead-horse Date: Wed, 17 Jan 2018 15:36:02 +0800 Subject: [PATCH 20/24] fix: ensure store expired after cookie --- lib/context.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/context.js b/lib/context.js index 5de4553..a6a2790 100644 --- a/lib/context.js +++ b/lib/context.js @@ -252,9 +252,9 @@ class ContextSession { // save if opts.renew and session will expired if (this.opts.renew) { const expire = session._expire; + const maxAge = session.maxAge; // renew when session will expired in maxAge / 2 - if (!expire || expire - Date.now() > session.maxAge / 2) return ''; - return 'renew'; + if (expire && maxAge && expire - Date.now() < maxAge / 2) return 'renew'; } return ''; @@ -286,7 +286,7 @@ class ContextSession { const externalKey = this.externalKey; let json = this.session.toJSON(); // set expire for check - const maxAge = opts.maxAge ? opts.maxAge : ONE_DAY; + let maxAge = opts.maxAge ? opts.maxAge : ONE_DAY; if (maxAge === 'session') { // do not set _expire in json if maxAge is set to 'session' // also delete maxAge from options @@ -300,6 +300,10 @@ class ContextSession { // save to external store if (externalKey) { debug('save %j to external key %s', json, externalKey); + if (typeof maxAge === 'number') { + // ensure store expired after cookie + maxAge += 10000; + } yield this.store.set(externalKey, json, maxAge, { changed, rolling: opts.rolling, From 3575adb56ea622aa6eac01a5ee5630bd26d9abee Mon Sep 17 00:00:00 2001 From: dead-horse Date: Wed, 17 Jan 2018 15:44:27 +0800 Subject: [PATCH 21/24] Release 4.8.0 --- History.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index c685320..ca5b8a1 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,13 @@ +4.8.0 / 2018-01-17 +================== + +**features** + * [[`ca6b329`](http://github.com/koajs/session/commit/ca6b32906678b3cf6168c4afac250b2e68fd17c8)] - feat: support opts.renew (#111) (Yiyu He <>) + +**fixes** + * [[`5c2fc72`](http://github.com/koajs/session/commit/5c2fc72cab47450da2c9ae8fedba8657d0c264ee)] - fix: ensure store expired after cookie (dead-horse <>) + 4.7.1 / 2018-01-11 ================== diff --git a/package.json b/package.json index 0b1e112..47aae65 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware", "repository": "koajs/session", - "version": "4.7.1", + "version": "4.8.0", "keywords": [ "koa", "middleware", From d7927708848a0e39a0652643ed3b6426ceb4952c Mon Sep 17 00:00:00 2001 From: killa Date: Tue, 18 Dec 2018 11:33:50 +0800 Subject: [PATCH 22/24] feat: allow init multi session middleware (#160) --- index.js | 3 +++ lib/util.js | 4 ++-- test/cookie.test.js | 13 +++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 92dcc81..4ba0783 100644 --- a/index.js +++ b/index.js @@ -113,6 +113,9 @@ function formatOpts(opts) { */ function extendContext(context, opts) { + if (context.hasOwnProperty(CONTEXT_SESSION)) { + return; + } Object.defineProperties(context, { [CONTEXT_SESSION]: { get() { diff --git a/lib/util.js b/lib/util.js index 2bdd7b8..759197c 100644 --- a/lib/util.js +++ b/lib/util.js @@ -13,7 +13,7 @@ module.exports = { */ decode(string) { - const body = new Buffer(string, 'base64').toString('utf8'); + const body = Buffer.from(string, 'base64').toString('utf8'); const json = JSON.parse(body); return json; }, @@ -28,7 +28,7 @@ module.exports = { encode(body) { body = JSON.stringify(body); - return new Buffer(body).toString('base64'); + return Buffer.from(body).toString('base64'); }, hash(sess) { diff --git a/test/cookie.test.js b/test/cookie.test.js index d306f26..5bdec4d 100644 --- a/test/cookie.test.js +++ b/test/cookie.test.js @@ -1,5 +1,6 @@ 'use strict'; +const assert = require('assert'); const koa = require('koa'); const request = require('supertest'); const should = require('should'); @@ -795,6 +796,18 @@ describe('Koa Session Cookie', () => { }); }); }); + + describe('init multi session middleware', () => { + it('should work', () => { + const app = new koa(); + + app.keys = [ 'a', 'b' ]; + const s1 = session({}, app); + const s2 = session({}, app); + assert(s1); + assert(s2); + }); + }); }); function App(options) { From d7a0e04ed85a33f61d5d95aa0e97796889f6626a Mon Sep 17 00:00:00 2001 From: dead-horse Date: Tue, 18 Dec 2018 11:35:43 +0800 Subject: [PATCH 23/24] Release 4.8.1 --- History.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index ca5b8a1..0368766 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,10 @@ +4.8.1 / 2018-12-18 +================== + +**features** + * [[`d792770`](http://github.com/koajs/session/commit/d7927708848a0e39a0652643ed3b6426ceb4952c)] - feat: allow init multi session middleware (#160) (killa <>) + 4.8.0 / 2018-01-17 ================== diff --git a/package.json b/package.json index 47aae65..bfef464 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "koa-session", "description": "Koa cookie session middleware", "repository": "koajs/session", - "version": "4.8.0", + "version": "4.8.1", "keywords": [ "koa", "middleware", From ec07a7555589b62030395a533764d220c1a145f4 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Mon, 29 Apr 2019 17:33:15 +0800 Subject: [PATCH 24/24] chore: change tag to latest-4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bfef464..17fb1fa 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "node": ">=4" }, "publishConfig": { - "tag": "v4" + "tag": "latest-4" }, "scripts": { "test": "npm run lint && NODE_ENV=test mocha --require should --reporter spec test/*.test.js",