next-session
Advanced tools
Comparing version 2.1.1 to 2.2.0
# Changelog | ||
## 2.2.0 | ||
### Minor | ||
- Allow manual session commit (#59) | ||
### Patches | ||
- Futhur check for headersSent (dd561a71c12ab6248b02873dd50cb114046be430) | ||
## 2.1.1 | ||
# Patches | ||
### Patches | ||
@@ -7,0 +17,0 @@ - Only warn if store is memoryStore (1d53f7d39c48e14288b738437c4577767534c08b) |
{ | ||
"name": "next-session", | ||
"version": "2.1.1", | ||
"version": "2.2.0", | ||
"description": "Simple promise-based session middleware for Next.js", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
# next-session | ||
[![npm](https://badgen.net/npm/v/next-session)](https://www.npmjs.com/package/next-session) | ||
[![minified size](https://badgen.net/bundlephobia/min/next-session)](https://bundlephobia.com/result?p=next-session) | ||
[![install size](https://packagephobia.now.sh/badge?p=next-session@2.1.0)](https://packagephobia.now.sh/result?p=next-session@2.1.0) | ||
[![CircleCI](https://circleci.com/gh/hoangvvo/next-session.svg?style=svg)](https://circleci.com/gh/hoangvvo/next-session) | ||
@@ -218,3 +218,3 @@ [![codecov](https://codecov.io/gh/hoangvvo/next-session/branch/master/graph/badge.svg)](https://codecov.io/gh/hoangvvo/next-session) | ||
Page.getInitialProps = async ({ req, res }) => await useSession(req, res); | ||
Page.getInitialProps = ({ req, res }) => useSession(req, res); | ||
``` | ||
@@ -231,5 +231,6 @@ | ||
| storePromisify | Promisify stores that are callback based. This allows you to use `next-session` with Connect stores (ex. used in [express-session](https://github.com/expressjs/session)) | `false` | | ||
| generateId | The function to generate a new session ID. This needs to return a string. | `crypto.randomBytes(16).toString('hex')` | | ||
| genid | The function to generate a new session ID. This needs to return a string. | `crypto.randomBytes(16).toString('hex')` | | ||
| rolling | Force the cookie to be set on every request despite no modification, extending the life time of the cookie in the browser | `false` | | ||
| touchAfter | On every request, the session store extends the life time of the session even when no changes are made (The same is done to Cookie). However, this may increase the load of the database. Setting this value will ask the store to only do so an amount of time since the Cookie is touched, with exception that the session is modified. Setting the value to `-1` will disable `touch()`. | `0` (Touch every time) | | ||
| touchAfter | On every request, session's life time are usually extended despite no changes. This value defer the process (to lower database load). Disable `touch()` by setting this to `-1`. | `0` (Touch every time) | | ||
| autoCommit | Automatically save session and set cookie header | `true` | | ||
| cookie.secure | Specifies the boolean value for the **Secure** `Set-Cookie` attribute. If set to true, cookie is only sent to the server with an encrypted request over the HTTPS protocol. | `false` | | ||
@@ -255,2 +256,6 @@ | cookie.httpOnly | Specifies the boolean value for the **httpOnly** `Set-Cookie` attribute. If set to true, cookies are inaccessible to client-side scripts. This is to help mitigate [cross-site scripting (XSS) attacks](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting). | `true` | | ||
#### req.session.id | ||
The unique id that associates to the current session. | ||
#### req.session.destroy() | ||
@@ -264,5 +269,5 @@ | ||
#### req.session.id | ||
#### req.session.commit() | ||
The unique id that associates to the current session. This should not be modified. | ||
If `options.autoCommit` is `false`, call this to save session to store and set cookie header. | ||
@@ -269,0 +274,0 @@ ### Session Store |
185
src/index.js
@@ -11,37 +11,64 @@ /* eslint-disable no-param-reassign */ | ||
const generateSessionId = () => crypto.randomBytes(16).toString('hex'); | ||
function proxyEnd(res, fn) { | ||
let ended = false; | ||
const oldEnd = res.end; | ||
res.end = function resEndProxy(...args) { | ||
const self = this; | ||
if (res.headersSent || res.finished || ended) return; | ||
ended = true; | ||
fn(() => { | ||
oldEnd.apply(self, args); | ||
}); | ||
}; | ||
} | ||
const hash = (sess) => { | ||
const str = JSON.stringify(sess, (key, val) => { | ||
if (key === 'cookie') { | ||
// filtered out session.cookie | ||
return undefined; | ||
} | ||
return val; | ||
}); | ||
// hash | ||
return crypto | ||
.createHash('sha1') | ||
.update(str, 'utf8') | ||
.digest('hex'); | ||
}; | ||
let storeReady = true; | ||
const session = (options = {}) => { | ||
const name = options.name || 'sessionId'; | ||
const cookieOptions = options.cookie || {}; | ||
const store = options.store || new MemoryStore(); | ||
const generateId = options.generateId || generateSessionId; | ||
const touchAfter = options.touchAfter ? parseToMs(options.touchAfter) : 0; | ||
const rollingSession = options.rolling || false; | ||
const storePromisify = options.storePromisify || false; | ||
async function initialize(req, res, options) { | ||
// eslint-disable-next-line no-multi-assign | ||
const originalId = req.sessionId = req.headers && req.headers.cookie | ||
? parseCookie(req.headers.cookie)[options.name] | ||
: null; | ||
// Notify MemoryStore should not be used in production | ||
// eslint-disable-next-line no-console | ||
if (store instanceof MemoryStore) console.warn('MemoryStore should not be used in production environment.'); | ||
req.sessionStore = options.store; | ||
// Validate parameters | ||
if (typeof generateId !== 'function') throw new TypeError('generateId option must be a function'); | ||
if (req.sessionId) { | ||
const sess = await req.sessionStore.get(req.sessionId); | ||
if (sess) req.sessionStore.createSession(req, res, sess); | ||
} | ||
if (!req.session) req.sessionStore.generate(req, res, options.generateId(), options.cookie); | ||
req._session = { | ||
// FIXME: Possible dataloss | ||
original: JSON.parse(JSON.stringify(req.session)), | ||
originalId, | ||
options, | ||
}; | ||
// autocommit | ||
if (options.autoCommit) { | ||
proxyEnd(res, async (done) => { | ||
if (req.session) { await req.session.commit(); } | ||
done(); | ||
}); | ||
} | ||
return req.session; | ||
} | ||
function session(opts = {}) { | ||
const options = { | ||
name: opts.name || 'sessionId', | ||
store: opts.store || new MemoryStore(), | ||
storePromisify: opts.storePromisify || false, | ||
generateId: opts.genid || opts.generateId || function generateId() { return crypto.randomBytes(16).toString('hex'); }, | ||
rolling: opts.rolling || false, | ||
touchAfter: opts.touchAfter ? parseToMs(opts.touchAfter) : 0, | ||
cookie: opts.cookie || {}, | ||
autoCommit: typeof opts.autoCommit !== 'undefined' ? opts.autoCommit : true, | ||
}; | ||
const { store, storePromisify } = options; | ||
// Promisify callback-based store. | ||
@@ -63,91 +90,11 @@ if (storePromisify) { | ||
return (req, res, next) => { | ||
/** | ||
* Modify req and res to "inject" the middleware | ||
*/ | ||
if (req.session) return next(); | ||
// check for store readiness before proceeded | ||
if (!storeReady) return next(); | ||
return async (req, res, next) => { | ||
if (req.session || !storeReady) { next(); return; } | ||
// TODO: add pathname mismatch check | ||
// Expose store | ||
req.sessionStore = store; | ||
// Try parse cookie if not already | ||
req.cookies = req.cookies | ||
|| (req.headers && typeof req.headers.cookie === 'string' && parseCookie(req.headers.cookie)) || {}; | ||
// Get sessionId cookie from Next.js parsed req.cookies | ||
req.sessionId = req.cookies[name]; | ||
const getSession = () => { | ||
// Return a session object | ||
if (!req.sessionId) { | ||
// If no sessionId found in Cookie header, generate one | ||
return Promise.resolve(hash(req.sessionStore.generate(req, generateId(), cookieOptions))); | ||
} | ||
return req.sessionStore.get(req.sessionId) | ||
.then((sess) => { | ||
if (sess) { | ||
return hash(req.sessionStore.createSession(req, sess)); | ||
} | ||
return hash(req.sessionStore.generate(req, generateId(), cookieOptions)); | ||
}); | ||
}; | ||
return getSession().then((hashedsess) => { | ||
let sessionSaved = false; | ||
const oldEnd = res.end; | ||
let ended = false; | ||
// Proxy res.end | ||
res.end = function resEndProxy(...args) { | ||
// If res.end() is called multiple times, do nothing after the first time | ||
if (ended) { | ||
return false; | ||
} | ||
ended = true; | ||
// save session to store if there are changes (and there is a session) | ||
const saveSession = () => { | ||
if (req.session) { | ||
if (hash(req.session) !== hashedsess) { | ||
sessionSaved = true; | ||
return req.session.save(); | ||
} | ||
// Touch: extend session time despite no modification | ||
if (req.session.cookie.maxAge && touchAfter >= 0) { | ||
const minuteSinceTouched = ( | ||
req.session.cookie.maxAge | ||
- (req.session.cookie.expires - new Date()) | ||
); | ||
if ((minuteSinceTouched < touchAfter)) { | ||
return Promise.resolve(); | ||
} | ||
return req.session.touch(); | ||
} | ||
} | ||
return Promise.resolve(); | ||
}; | ||
return saveSession() | ||
.then(() => { | ||
if ( | ||
(req.cookies[name] !== req.sessionId || sessionSaved || rollingSession) | ||
&& req.session | ||
) { | ||
res.setHeader('Set-Cookie', req.session.cookie.serialize(name, req.sessionId)); | ||
} | ||
oldEnd.apply(this, args); | ||
}); | ||
}; | ||
next(); | ||
}); | ||
await initialize(req, res, options); | ||
next(); | ||
}; | ||
}; | ||
} | ||
const useSession = (req, res, opts) => { | ||
function useSession(req, res, opts) { | ||
if (!req || !res) return Promise.resolve(); | ||
@@ -161,9 +108,8 @@ return new Promise((resolve) => { | ||
}); | ||
}; | ||
} | ||
const withSession = (handler, options) => { | ||
function withSession(handler, options) { | ||
const isApiRoutes = !Object.prototype.hasOwnProperty.call(handler, 'getInitialProps'); | ||
const oldHandler = (isApiRoutes) ? handler : handler.getInitialProps; | ||
function handlerProxy(...args) { | ||
@@ -188,5 +134,6 @@ let req; | ||
return handler; | ||
}; | ||
} | ||
module.exports = session; | ||
module.exports.initialize = initialize; | ||
module.exports.withSession = withSession; | ||
@@ -193,0 +140,0 @@ module.exports.useSession = useSession; |
@@ -0,4 +1,7 @@ | ||
function stringify(sess) { return JSON.stringify(sess, (key, val) => (key === 'cookie' ? undefined : val)); } | ||
class Session { | ||
constructor(req, sess) { | ||
constructor(req, res, sess) { | ||
Object.defineProperty(this, 'req', { value: req }); | ||
Object.defineProperty(this, 'res', { value: res }); | ||
Object.defineProperty(this, 'id', { value: req.sessionId }); | ||
@@ -32,4 +35,28 @@ if (typeof sess === 'object') { | ||
} | ||
async commit() { | ||
const { name, rolling, touchAfter } = this.req._session.options; | ||
let saved = false; | ||
if (stringify(this) !== stringify(this.req._session.original)) { | ||
await this.save(); | ||
saved = true; | ||
} | ||
// Touch: extend session time despite no modification | ||
if (this.cookie.maxAge && touchAfter >= 0) { | ||
const minuteSinceTouched = ( | ||
this.cookie.maxAge | ||
- (this.cookie.expires - new Date()) | ||
); | ||
if ((minuteSinceTouched >= touchAfter)) await this.touch(); | ||
} | ||
if ( | ||
(saved || rolling || this.req._session.originalId !== this.req.sessionId) | ||
&& this | ||
) this.res.setHeader('Set-Cookie', this.cookie.serialize(name, this.req.sessionId)); | ||
} | ||
} | ||
module.exports = Session; |
@@ -12,5 +12,5 @@ const util = require('util'); | ||
Store.prototype.generate = function generate(req, genId, cookieOptions) { | ||
Store.prototype.generate = function generate(req, res, genId, cookieOptions) { | ||
req.sessionId = genId; | ||
req.session = new Session(req); | ||
req.session = new Session(req, res); | ||
req.session.cookie = new Cookie(cookieOptions); | ||
@@ -20,3 +20,3 @@ return req.session; | ||
Store.prototype.createSession = function createSession(req, sess) { | ||
Store.prototype.createSession = function createSession(req, res, sess) { | ||
const thisSess = sess; | ||
@@ -26,3 +26,3 @@ const { expires } = thisSess.cookie; | ||
if (typeof expires === 'string') thisSess.cookie.expires = new Date(expires); | ||
req.session = new Session(req, thisSess); | ||
req.session = new Session(req, res, thisSess); | ||
return req.session; | ||
@@ -29,0 +29,0 @@ }; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
295
27830
308