From 711392959a798886290333bc57d124bf00b4944b Mon Sep 17 00:00:00 2001 From: Vincent Voyer Date: Fri, 1 May 2020 21:51:59 +0200 Subject: [PATCH] feat(API): multiple changes (#83) --- .eslintignore | 3 +- .gitignore | 4 +- .vscode/settings.json | 2 +- README.md | 141 +++- examples/express/README.md | 30 + examples/express/app.js | 40 + examples/express/bin/www | 90 +++ examples/express/package.json | 15 + examples/express/public/stylesheets/style.css | 8 + examples/express/routes/index.js | 46 ++ examples/express/routes/users.js | 9 + examples/express/views/error.jade | 6 + examples/express/views/index.jade | 10 + examples/express/views/layout.jade | 7 + examples/express/views/profile.jade | 8 + examples/express/views/restricted.jade | 6 + examples/express/yarn.lock | 688 ++++++++++++++++++ {example => examples/next.js}/.gitignore | 0 .../next.js}/.yarn/releases/yarn-1.22.4.js | 0 {example => examples/next.js}/.yarnrc | 0 {example => examples/next.js}/README.md | 2 +- .../next.js}/components/Form.jsx | 0 .../next.js}/components/Header.jsx | 0 .../next.js}/components/Layout.jsx | 0 .../next.js}/lib/fetchJson.js | 0 {example => examples/next.js}/lib/session.js | 4 +- {example => examples/next.js}/lib/useUser.js | 0 {example => examples/next.js}/package.json | 0 {example => examples/next.js}/pages/_app.jsx | 0 .../next.js}/pages/api/login.js | 0 .../next.js}/pages/api/logout.js | 0 .../next.js}/pages/api/user.js | 0 {example => examples/next.js}/pages/index.jsx | 0 {example => examples/next.js}/pages/login.jsx | 0 .../next.js}/pages/profile-sg.jsx | 0 .../next.js}/pages/profile-ssr.jsx | 0 {example => examples/next.js}/yarn.lock | 0 lib/index.js | 124 ++-- lib/index.test.js | 230 ++++-- package.json | 1 + 40 files changed, 1362 insertions(+), 112 deletions(-) create mode 100644 examples/express/README.md create mode 100644 examples/express/app.js create mode 100755 examples/express/bin/www create mode 100644 examples/express/package.json create mode 100644 examples/express/public/stylesheets/style.css create mode 100644 examples/express/routes/index.js create mode 100644 examples/express/routes/users.js create mode 100644 examples/express/views/error.jade create mode 100644 examples/express/views/index.jade create mode 100644 examples/express/views/layout.jade create mode 100644 examples/express/views/profile.jade create mode 100644 examples/express/views/restricted.jade create mode 100644 examples/express/yarn.lock rename {example => examples/next.js}/.gitignore (100%) rename {example => examples/next.js}/.yarn/releases/yarn-1.22.4.js (100%) rename {example => examples/next.js}/.yarnrc (100%) rename {example => examples/next.js}/README.md (96%) rename {example => examples/next.js}/components/Form.jsx (100%) rename {example => examples/next.js}/components/Header.jsx (100%) rename {example => examples/next.js}/components/Layout.jsx (100%) rename {example => examples/next.js}/lib/fetchJson.js (100%) rename {example => examples/next.js}/lib/session.js (88%) rename {example => examples/next.js}/lib/useUser.js (100%) rename {example => examples/next.js}/package.json (100%) rename {example => examples/next.js}/pages/_app.jsx (100%) rename {example => examples/next.js}/pages/api/login.js (100%) rename {example => examples/next.js}/pages/api/logout.js (100%) rename {example => examples/next.js}/pages/api/user.js (100%) rename {example => examples/next.js}/pages/index.jsx (100%) rename {example => examples/next.js}/pages/login.jsx (100%) rename {example => examples/next.js}/pages/profile-sg.jsx (100%) rename {example => examples/next.js}/pages/profile-ssr.jsx (100%) rename {example => examples/next.js}/yarn.lock (100%) diff --git a/.eslintignore b/.eslintignore index 55f21e07..d1d885b4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ -example/ +examples/next.js/ +examples/express/node_modules dist/ diff --git a/.gitignore b/.gitignore index 215e23a1..ac678bd3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,11 @@ node_modules/ # misc .DS_Store /dist/ -/example/.next/ +/examples/next.js/.next/ # debug npm-debug.log* yarn-debug.log* yarn-error.log* -.now \ No newline at end of file +.now diff --git a/.vscode/settings.json b/.vscode/settings.json index 2e3ae787..7cb8e910 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "editor.formatOnSave": true, "search.exclude": { ".yarn": true, - "example/.yarn": true, + "examples/next.js/.yarn": true, "yarn.lock": true }, "eslint.packageManager": "yarn", diff --git a/README.md b/README.md index e36fc616..6c7f889e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # next-iron-session [![GitHub license](https://img.shields.io/github/license/vvo/next-iron-session?style=flat)](https://github.com/vvo/next-iron-session/blob/master/LICENSE) [![Tests](https://github.com/vvo/next-iron-session/workflows/Tests/badge.svg)](https://github.com/vvo/next-iron-session/actions) [![codecov](https://codecov.io/gh/vvo/next-iron-session/branch/master/graph/badge.svg)](https://codecov.io/gh/vvo/next-iron-session) ![npm](https://img.shields.io/npm/v/next-iron-session) -_🛠 Next.js stateless session utility using signed and encrypted cookies to store data_ +_🛠 Next.js and Express (connect middleware) stateless session utility using signed and encrypted cookies to store data_ --- -**This [Next.js](https://nextjs.org/) backend utility** allows you to create a session to then be stored in browser cookies via a signed and encrypted seal. This provides client sessions that are ⚒️ iron-strong. +**This [Next.js](https://nextjs.org/), [Express](https://expressjs.com/) and [Connect](https://github.com/senchalabs/connect) backend utility** allows you to create a session to then be stored in browser cookies via a signed and encrypted seal. This provides client sessions that are ⚒️ iron-strong. The seal stored on the client contains the session data, not your server, making it a "stateless" session from the server point of view. This is a different take than [next-session](https://github.com/hoangvvo/next-session/) where the cookie contains a session ID to then be used to map data on the server-side. @@ -15,42 +15,48 @@ The seal stored on the client contains the session data, not your server, making The seal is signed and encrypted using [@hapi/iron](https://github.com/hapijs/iron), [iron-store](https://github.com/vvo/iron-store/) is used behind the scenes. This method of storing session data is the same technique used by **frameworks like [Ruby On Rails](https://guides.rubyonrails.org/security.html#session-storage)**. -**⚡️ Flash session data is supported**. It means you can store some data which will be deleted when read. This is useful for temporary data, redirects or notices on your UI. - **♻️ Password rotation is supported**. It allows you to change the password used to sign and encrypt sessions while still being able to decrypt sessions that were created with a previous password. -**By default the cookie has an ⏰ expiration time of 15 days**, set via [`maxAge`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Directives). After that, even if someone tries to reuse the cookie, `next-iron-session` will not accept the underlying seal because the expiration is part of the seal value. See https://hapi.dev/family/iron for more information on @hapi/iron mechanisms. - **Next.js's** 🗿 [Static generation](https://nextjs.org/docs/basic-features/pages#static-generation-recommended) (SG) and ⚙️ [Server-side Rendering](https://nextjs.org/docs/basic-features/pages#server-side-rendering) (SSG) are both supported. +**There's a Connect middleware available** so you can use this library in any Connect compatible framework like Express. + +**By default the cookie has an ⏰ expiration time of 15 days**, set via [`maxAge`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Directives). After that, even if someone tries to reuse the cookie, `next-iron-session` will not accept the underlying seal because the expiration is part of the seal value. See https://hapi.dev/family/iron for more information on @hapi/iron mechanisms. + _Table of contents:_ - [Installation](#installation) - [Usage](#usage) - [Examples](#examples) - [Handle password rotation/update the password](#handle-password-rotationupdate-the-password) + - [Express / Connect middleware: `ironSession`](#express--connect-middleware-ironsession) + - [Usage with `next-connect`](#usage-with-next-connect) - [API](#api) - - [withIronSession(handler, { password, ttl, cookieName, cookieOptions })](#withironsessionhandler--password-ttl-cookiename-cookieoptions-) + - [withIronSession(handler, { password, cookieName, [ttl], [cookieOptions] })](#withironsessionhandler--password-cookiename-ttl-cookieoptions-) + - [ironSession({ password, cookieName, [ttl], [cookieOptions] })](#ironsession-password-cookiename-ttl-cookieoptions-) + - [async applySession(req, res, { password, cookieName, [ttl], [cookieOptions] })](#async-applysessionreq-res--password-cookiename-ttl-cookieoptions-) - [req.session.set(name, value)](#reqsessionsetname-value) - [req.session.get(name)](#reqsessiongetname) - - [req.session.setFlash(name, value)](#reqsessionsetflashname-value) - [req.session.unset(name)](#reqsessionunsetname) - [req.session.destroy()](#reqsessiondestroy) - [FAQ](#faq) - [Why use pure 🍪 cookies for sessions?](#why-use-pure--cookies-for-sessions) - [How is this different from JWT?](#how-is-this-different-from-jwt) - [Project status](#project-status) +- [Credits](#credits) - [🤓 References](#-references) ## Installation ```bash npm add next-iron-session + +yarn add next-iron-session ``` ## Usage -You can find a more complete real-world example in the [example folder](./example/). +You can find real-world examples (Next.js, Express) in the [examples folder](./examples/). The password is a private key you must pass at runtime, it has to be at least 32 characters long. Use https://1password.com/password-generator/ to generate strong passwords. @@ -59,7 +65,7 @@ The password is a private key you must pass at runtime, it has to be at least 32 **pages/api/login.js**: ```js -import withIronSession from "iron-session"; +import { withIronSession } from "next-iron-session"; async function handler(req, res) { // get user from database then: @@ -73,13 +79,17 @@ async function handler(req, res) { export default withIronSession(handler, { password: "complex_password_at_least_32_characters_long", + // if your localhost is server on http:// then disable the secure flag + cookieOptions: { + secure: process.env.NODE_ENV === "production" ? true : false, + }, }); ``` **pages/user.js**: ```js -import withIronSession from "iron-session"; +import { withIronSession } from "next-iron-session"; function handler(req, res, session) { const user = req.session.get("user"); @@ -88,13 +98,17 @@ function handler(req, res, session) { export default withIronSession(handler, { password: "complex_password_at_least_32_characters_long", + // if your localhost is server on http:// then disable the secure flag + cookieOptions: { + secure: process.env.NODE_ENV === "production" ? true : false, + }, }); ``` **pages/api/logout.js**: ```js -import withIronSession from "iron-session"; +import { withIronSession } from "next-iron-session"; function handler(req, res, session) { req.session.destroy(); @@ -103,9 +117,19 @@ function handler(req, res, session) { export default withIronSession(handler, { password: "complex_password_at_least_32_characters_long", + // if your localhost is server on http:// then disable the secure flag + cookieOptions: { + secure: process.env.NODE_ENV === "production" ? true : false, + }, }); ``` +⚠️ Sessions are automatically recreated (empty session though) when: + +- they expire +- a wrong password was used +- we can't find back the password id in the current list + ## Examples ### Handle password rotation/update the password @@ -153,13 +177,74 @@ Notes: - The password used to encrypt session data (to `seal`) is always the first one in the array, so when rotating to put a new password, it must be first in the array list - Even if you do not provide an array at first, you can always move to array based passwords afterwards, knowing that your first password (`string`) was given `{id:1}` automatically. +### Express / Connect middleware: `ironSession` + +You can import and use `ironSession` if you want to use `next-iron-session` in [Express](https://expressjs.com/) and [Connect](https://github.com/senchalabs/connect). + +```js +import { ironSession } from "next-iron-session"; + +const session = ironSession({ + cookieName: "next-iron-session/examples/express", + password: process.env.SECRET_COOKIE_PASSWORD, + // if your localhost is server on http:// then disable the secure flag + cookieOptions: { + secure: process.env.NODE_ENV === "production" ? true : false, + }, +}); + +router.get("/profile", session, async function (req, res) { + // now you can access all of the req.session.* utilities + if (req.session.get("user") === undefined) { + res.redirect("/restricted"); + return; + } + + res.render("profile", { + title: "Profile", + userId: req.session.get("user").id, + }); +}); +``` + +A more complete example using Express can be found in the [examples folder](./examples/express). + +### Usage with `next-connect` + +Since `ironSession` is an Express / Connect middleware, it means you can use it with [`next-connect`](https://github.com/hoangvvo/next-connect): + +```js +import { ironSession } from "next-iron-session"; + +const session = ironSession({ + cookieName: "next-iron-session/examples/express", + password: process.env.SECRET_COOKIE_PASSWORD, + // if your localhost is server on http:// then disable the secure flag + cookieOptions: { + secure: process.env.NODE_ENV === "production" ? true : false, + }, +}); +import nextConnect from "next-connect"; + +const handler = nextConnect(); + +handler.use(session).get((req, res) => { + const user = req.session.get("user"); + res.send(`Hello user ${user.id}`); +}); + +export default handler; +``` + ## API -### withIronSession(handler, { password, ttl, cookieName, cookieOptions }) +### withIronSession(handler, { password, cookieName, [ttl], [cookieOptions] }) + +This can be used to wrap Next.js [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering) or [API Routes](https://nextjs.org/docs/api-routes/introduction) so you can then access all `req.session.*` methods. - `password`, **required**: Private key used to encrypt the cookie. It has to be at least 32 characters long. Use https://1password.com/password-generator/ to generate strong passwords. `password` can be either a `string` or an `array` of objects like this: `[{id: 2, password: "..."}, {id: 1, password: "..."}]` to allow for password rotation. +- `cookieName`, **required**: Name of the cookie to be stored - `ttl`, _optional_: In seconds, default to 14 days -- `cookieName`, _optional_: Default to `__ironSession` - `cookieOptions`, _optional_: Any option available from [jshttp/cookie#serialize](https://github.com/jshttp/cookie#cookieserializename-value-options). Default to: ```js @@ -173,12 +258,30 @@ Notes: } ``` +### ironSession({ password, cookieName, [ttl], [cookieOptions] }) + +Connect middleware. + +```js +import { ironSession } from "next-iron-session"; + +app.use(ironSession({ ...options })); +``` + +### async applySession(req, res, { password, cookieName, [ttl], [cookieOptions] }) + +Allows you to use this module the way you want as long as you have access to `req` and `res`. + +```js +import { applySession } from "next-session"; + +await applySession(req, res, options); +``` + ### req.session.set(name, value) ### req.session.get(name) -### req.session.setFlash(name, value) - ### req.session.unset(name) ### req.session.destroy() @@ -213,6 +316,12 @@ This is a recent library I authored because I needed it. While @hapi/iron is bat If you find bugs or have API ideas, [create an issue](https://github.com/vvo/next-iron-session/issues). +## Credits + +Thanks to [Hoang Vo](https://github.com/hoangvvo) for advice and guidance while building this module. Hoang built [next-connect](https://github.com/hoangvvo/next-connect) and [next-session](https://github.com/hoangvvo/next-session). + +Thanks to [hapi](https://hapi.dev/) team for creating [iron](https://github.com/hapijs/iron). + ## 🤓 References - https://owasp.org/www-project-cheat-sheets/cheatsheets/Session_Management_Cheat_Sheet.html#cookies diff --git a/examples/express/README.md b/examples/express/README.md new file mode 100644 index 00000000..53242f33 --- /dev/null +++ b/examples/express/README.md @@ -0,0 +1,30 @@ +# Express example application using next-iron-session + +This is a small example application generated with the [Express application generator](https://expressjs.com/en/starter/generator.html). + +The tl;dr; on how to use `next-iron-session` with Express is this: + +```js +import { ironSession } from "next-iron-session"; + +const session = ironSession({ + cookieName: "next-iron-session/examples/express", + password: process.env.SECRET_COOKIE_PASSWORD, + cookieOptions: { + secure: process.env.NODE_ENV === "production" ? true : false, + }, +}); + +router.get("/profile", session, async function (req, res) { + // now you can access all of the req.session.* utilities + if (req.session.get("user") === undefined) { + res.redirect("/restricted"); + return; + } + + res.render("profile", { + title: "Profile", + userId: req.session.get("user").id, + }); +}); +``` diff --git a/examples/express/app.js b/examples/express/app.js new file mode 100644 index 00000000..d427496c --- /dev/null +++ b/examples/express/app.js @@ -0,0 +1,40 @@ +var path = require("path"); + +var createError = require("http-errors"); +var express = require("express"); +var logger = require("morgan"); + +var indexRouter = require("./routes/index"); +var usersRouter = require("./routes/users"); + +var app = express(); + +// view engine setup +app.set("views", path.join(__dirname, "views")); +app.set("view engine", "jade"); + +app.use(logger("dev")); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(express.static(path.join(__dirname, "public"))); + +app.use("/", indexRouter); +app.use("/users", usersRouter); + +// catch 404 and forward to error handler +app.use(function (req, res, next) { + next(createError(404)); +}); + +// error handler +app.use(function (err, req, res) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get("env") === "development" ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render("error"); +}); + +module.exports = app; diff --git a/examples/express/bin/www b/examples/express/bin/www new file mode 100755 index 00000000..05cf405e --- /dev/null +++ b/examples/express/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('express:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/examples/express/package.json b/examples/express/package.json new file mode 100644 index 00000000..3a01d7a3 --- /dev/null +++ b/examples/express/package.json @@ -0,0 +1,15 @@ +{ + "name": "express", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "SECRET_COOKIE_PASSWORD=Zp-p!QhpyU_0q!@taxoceA.4q35B7@4q node ./bin/www" + }, + "dependencies": { + "debug": "~2.6.9", + "express": "~4.16.1", + "http-errors": "~1.6.3", + "jade": "~1.11.0", + "morgan": "~1.9.1" + } +} diff --git a/examples/express/public/stylesheets/style.css b/examples/express/public/stylesheets/style.css new file mode 100644 index 00000000..5b5051a2 --- /dev/null +++ b/examples/express/public/stylesheets/style.css @@ -0,0 +1,8 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00b7ff; +} diff --git a/examples/express/routes/index.js b/examples/express/routes/index.js new file mode 100644 index 00000000..61eeeff9 --- /dev/null +++ b/examples/express/routes/index.js @@ -0,0 +1,46 @@ +var express = require("express"); +var ironSession = require("next-iron-session").ironSession; + +var router = express.Router(); +var session = ironSession({ + cookieName: "next-iron-session/examples/express", + password: process.env.SECRET_COOKIE_PASSWORD, + cookieOptions: { + // the next line allows to use the session in non-https environements + secure: process.env.NODE_ENV === "production" ? true : false, + }, +}); + +/* GET home page. */ +router.get("/", function (req, res) { + res.render("index", { title: "Express" }); +}); + +router.get("/login", session, async function (req, res) { + req.session.set("user", { id: 20 }); + await req.session.save(); + res.redirect("/profile"); +}); + +router.get("/profile", session, async function (req, res) { + if (req.session.get("user") === undefined) { + res.redirect("/restricted"); + return; + } + + res.render("profile", { + title: "Profile", + userId: req.session.get("user").id, + }); +}); + +router.get("/logout", session, async function (req, res) { + req.session.destroy(); + res.redirect("/"); +}); + +router.get("/restricted", function (req, res) { + res.render("restricted", { title: "Restricted" }); +}); + +module.exports = router; diff --git a/examples/express/routes/users.js b/examples/express/routes/users.js new file mode 100644 index 00000000..3256dfef --- /dev/null +++ b/examples/express/routes/users.js @@ -0,0 +1,9 @@ +var express = require("express"); +var router = express.Router(); + +/* GET users listing. */ +router.get("/", function (req, res) { + res.send("respond with a resource"); +}); + +module.exports = router; diff --git a/examples/express/views/error.jade b/examples/express/views/error.jade new file mode 100644 index 00000000..51ec12c6 --- /dev/null +++ b/examples/express/views/error.jade @@ -0,0 +1,6 @@ +extends layout + +block content + h1= message + h2= error.status + pre #{error.stack} diff --git a/examples/express/views/index.jade b/examples/express/views/index.jade new file mode 100644 index 00000000..54f7ddd3 --- /dev/null +++ b/examples/express/views/index.jade @@ -0,0 +1,10 @@ +extends layout + +block content + h1= title + p Welcome to #{title} + a(href="/login") Login + br + a(href="/profile") Profile (protected) + br + a(href="/logout") Logout diff --git a/examples/express/views/layout.jade b/examples/express/views/layout.jade new file mode 100644 index 00000000..15af079b --- /dev/null +++ b/examples/express/views/layout.jade @@ -0,0 +1,7 @@ +doctype html +html + head + title= title + link(rel='stylesheet', href='/stylesheets/style.css') + body + block content diff --git a/examples/express/views/profile.jade b/examples/express/views/profile.jade new file mode 100644 index 00000000..c0ce6816 --- /dev/null +++ b/examples/express/views/profile.jade @@ -0,0 +1,8 @@ +extends layout + +block content + h1= title + p Welcome user #{userId} + a(href="/") Home + br + a(href="/logout") Logout diff --git a/examples/express/views/restricted.jade b/examples/express/views/restricted.jade new file mode 100644 index 00000000..53b78f3e --- /dev/null +++ b/examples/express/views/restricted.jade @@ -0,0 +1,6 @@ +extends layout + +block content + h1= title + p You need to login first + a(href="/login") Login diff --git a/examples/express/yarn.lock b/examples/express/yarn.lock new file mode 100644 index 00000000..682cf98a --- /dev/null +++ b/examples/express/yarn.lock @@ -0,0 +1,688 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +accepts@~1.3.5: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +acorn-globals@^1.0.3: + version "1.0.9" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-1.0.9.tgz#55bb5e98691507b74579d0513413217c380c54cf" + integrity sha1-VbtemGkVB7dFedBRNBMhfDgMVM8= + dependencies: + acorn "^2.1.0" + +acorn@^1.0.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-1.2.2.tgz#c8ce27de0acc76d896d2b1fad3df588d9e82f014" + integrity sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ= + +acorn@^2.1.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7" + integrity sha1-q259nYhqrKiwhbwzEreaGYQz8Oc= + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc= + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +asap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/asap/-/asap-1.0.0.tgz#b2a45da5fdfa20b0496fc3768cc27c12fa916a7d" + integrity sha1-sqRdpf36ILBJb8N2jMJ8EvqRan0= + +basic-auth@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +body-parser@1.18.3: + version "1.18.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" + integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "~1.6.3" + iconv-lite "0.4.23" + on-finished "~2.3.0" + qs "6.5.2" + raw-body "2.3.3" + type-is "~1.6.16" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk= + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60= + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +character-parser@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-1.2.1.tgz#c0dde4ab182713b919b970959a123ecc1a30fcd6" + integrity sha1-wN3kqxgnE7kZuXCVmhI+zBow/NY= + +clean-css@^3.1.9: + version "3.4.28" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff" + integrity sha1-vxlF6C/ICPVWlebd6uwBQA79A/8= + dependencies: + commander "2.8.x" + source-map "0.4.x" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE= + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +commander@2.8.x: + version "2.8.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" + integrity sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ= + dependencies: + graceful-readlink ">= 1.0.0" + +commander@~2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" + integrity sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0= + +constantinople@~3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-3.0.2.tgz#4b945d9937907bcd98ee575122c3817516544141" + integrity sha1-S5RdmTeQe82Y7ldRIsOBdRZUQUE= + dependencies: + acorn "^2.1.0" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-parser@~1.4.4: + version "1.4.5" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49" + integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw== + dependencies: + cookie "0.4.0" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +css-parse@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-1.0.4.tgz#38b0503fbf9da9f54e9c1dbda60e145c77117bdd" + integrity sha1-OLBQP7+dqfVOnB29pg4UXHcRe90= + +css-stringify@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/css-stringify/-/css-stringify-1.0.5.tgz#b0d042946db2953bb9d292900a6cb5f6d0122031" + integrity sha1-sNBClG2ylTu50pKQCmy19tASIDE= + +css@~1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/css/-/css-1.0.8.tgz#9386811ca82bccc9ee7fb5a732b1e2a317c8a3e7" + integrity sha1-k4aBHKgrzMnuf7WnMrHioxfIo+c= + dependencies: + css-parse "1.0.4" + css-stringify "1.0.5" + +debug@2.6.9, debug@~2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +decamelize@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +express@~4.16.1: + version "4.16.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" + integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== + dependencies: + accepts "~1.3.5" + array-flatten "1.1.1" + body-parser "1.18.3" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.1" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.4" + qs "6.5.2" + range-parser "~1.2.0" + safe-buffer "5.1.2" + send "0.16.2" + serve-static "1.13.2" + setprototypeof "1.1.0" + statuses "~1.4.0" + type-is "~1.6.16" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" + integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.4.0" + unpipe "~1.0.0" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= + +http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +iconv-lite@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-promise@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + +is-promise@~1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-1.0.1.tgz#31573761c057e33c2e91aab9e96da08cefbe76e5" + integrity sha1-MVc3YcBX4zwukaq56W2gjO++duU= + +jade@~1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/jade/-/jade-1.11.0.tgz#9c80e538c12d3fb95c8d9bb9559fa0cc040405fd" + integrity sha1-nIDlOMEtP7lcjZu5VZ+gzAQEBf0= + dependencies: + character-parser "1.2.1" + clean-css "^3.1.9" + commander "~2.6.0" + constantinople "~3.0.1" + jstransformer "0.0.2" + mkdirp "~0.5.0" + transformers "2.1.0" + uglify-js "^2.4.19" + void-elements "~2.0.1" + with "~4.0.0" + +jstransformer@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-0.0.2.tgz#7aae29a903d196cfa0973d885d3e47947ecd76ab" + integrity sha1-eq4pqQPRls+glz2IXT5HlH7Ndqs= + dependencies: + is-promise "^2.0.0" + promise "^6.0.1" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@~2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@~0.5.0: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +morgan@~1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.1.tgz#0a8d16734a1d9afbc824b99df87e738e58e2da59" + integrity sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA== + dependencies: + basic-auth "~2.0.0" + debug "2.6.9" + depd "~1.1.2" + on-finished "~2.3.0" + on-headers "~1.0.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +optimist@~0.3.5: + version "0.3.7" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" + integrity sha1-yQlBrVnkJzMokjB00s8ufLxuwNk= + dependencies: + wordwrap "~0.0.2" + +parseurl@~1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +promise@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/promise/-/promise-6.1.0.tgz#2ce729f6b94b45c26891ad0602c5c90e04c6eef6" + integrity sha1-LOcp9rlLRcJoka0GAsXJDgTG7vY= + dependencies: + asap "~1.0.0" + +promise@~2.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/promise/-/promise-2.0.0.tgz#46648aa9d605af5d2e70c3024bf59436da02b80e" + integrity sha1-RmSKqdYFr10ucMMCS/WUNtoCuA4= + dependencies: + is-promise "~1" + +proxy-addr@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +qs@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +range-parser@~1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8= + dependencies: + align-text "^0.1.1" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" + integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.4.0" + +serve-static@1.13.2: + version "1.13.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" + integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +source-map@0.4.x: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + integrity sha1-66T12pwNyZneaAMti092FzZSA2s= + dependencies: + amdefine ">=0.0.4" + +source-map@~0.1.7: + version "0.1.43" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" + integrity sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y= + dependencies: + amdefine ">=0.0.4" + +source-map@~0.5.1: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== + +transformers@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/transformers/-/transformers-2.1.0.tgz#5d23cb35561dd85dc67fb8482309b47d53cce9a7" + integrity sha1-XSPLNVYd2F3Gf7hIIwm0fVPM6ac= + dependencies: + css "~1.0.8" + promise "~2.0" + uglify-js "~2.2.5" + +type-is@~1.6.16: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +uglify-js@^2.4.19: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0= + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-js@~2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.2.5.tgz#a6e02a70d839792b9780488b7b8b184c095c99c7" + integrity sha1-puAqcNg5eSuXgEiLe4sYTAlcmcc= + dependencies: + optimist "~0.3.5" + source-map "~0.1.7" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +void-elements@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= + +with@~4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/with/-/with-4.0.3.tgz#eefd154e9e79d2c8d3417b647a8f14d9fecce14e" + integrity sha1-7v0VTp550sjTQXtkeo8U2f7M4U4= + dependencies: + acorn "^1.0.1" + acorn-globals "^1.0.3" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E= + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" diff --git a/example/.gitignore b/examples/next.js/.gitignore similarity index 100% rename from example/.gitignore rename to examples/next.js/.gitignore diff --git a/example/.yarn/releases/yarn-1.22.4.js b/examples/next.js/.yarn/releases/yarn-1.22.4.js similarity index 100% rename from example/.yarn/releases/yarn-1.22.4.js rename to examples/next.js/.yarn/releases/yarn-1.22.4.js diff --git a/example/.yarnrc b/examples/next.js/.yarnrc similarity index 100% rename from example/.yarnrc rename to examples/next.js/.yarnrc diff --git a/example/README.md b/examples/next.js/README.md similarity index 96% rename from example/README.md rename to examples/next.js/README.md index 47437ea3..24baaa36 100644 --- a/example/README.md +++ b/examples/next.js/README.md @@ -1,4 +1,4 @@ -# Example application using [`next-iron-session`](https://github.com/vvo/next-iron/session) +# Example Next.js application using [`next-iron-session`](https://github.com/vvo/next-iron/session) This example creates an authentication system that uses a **signed and encrypted cookie to store session data**. It relies on [`next-iron-session`](https://github.com/vvo/next-iron-session). diff --git a/example/components/Form.jsx b/examples/next.js/components/Form.jsx similarity index 100% rename from example/components/Form.jsx rename to examples/next.js/components/Form.jsx diff --git a/example/components/Header.jsx b/examples/next.js/components/Header.jsx similarity index 100% rename from example/components/Header.jsx rename to examples/next.js/components/Header.jsx diff --git a/example/components/Layout.jsx b/examples/next.js/components/Layout.jsx similarity index 100% rename from example/components/Layout.jsx rename to examples/next.js/components/Layout.jsx diff --git a/example/lib/fetchJson.js b/examples/next.js/lib/fetchJson.js similarity index 100% rename from example/lib/fetchJson.js rename to examples/next.js/lib/fetchJson.js diff --git a/example/lib/session.js b/examples/next.js/lib/session.js similarity index 88% rename from example/lib/session.js rename to examples/next.js/lib/session.js index a7b2d0e4..8a9594ad 100644 --- a/example/lib/session.js +++ b/examples/next.js/lib/session.js @@ -1,5 +1,5 @@ // this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions -import withIronSession from "next-iron-session"; +import { withIronSession } from "next-iron-session"; export default function withSession(handler) { return withIronSession(handler, { @@ -7,7 +7,7 @@ export default function withSession(handler) { // ⚠️ Do not reuse the same password, create a different password for you and store it in a secret management system // Example for Zeit's now: https://zeit.co/docs/v2/serverless-functions/env-and-secrets password: process.env.SECRET_COOKIE_PASSWORD, - cookieName: "next-iron-session/example", + cookieName: "next-iron-session/examples/next.js", cookieOptions: { // the next line allows to use the session in non-https environements like // Next.js dev mode (http://localhost:3000) diff --git a/example/lib/useUser.js b/examples/next.js/lib/useUser.js similarity index 100% rename from example/lib/useUser.js rename to examples/next.js/lib/useUser.js diff --git a/example/package.json b/examples/next.js/package.json similarity index 100% rename from example/package.json rename to examples/next.js/package.json diff --git a/example/pages/_app.jsx b/examples/next.js/pages/_app.jsx similarity index 100% rename from example/pages/_app.jsx rename to examples/next.js/pages/_app.jsx diff --git a/example/pages/api/login.js b/examples/next.js/pages/api/login.js similarity index 100% rename from example/pages/api/login.js rename to examples/next.js/pages/api/login.js diff --git a/example/pages/api/logout.js b/examples/next.js/pages/api/logout.js similarity index 100% rename from example/pages/api/logout.js rename to examples/next.js/pages/api/logout.js diff --git a/example/pages/api/user.js b/examples/next.js/pages/api/user.js similarity index 100% rename from example/pages/api/user.js rename to examples/next.js/pages/api/user.js diff --git a/example/pages/index.jsx b/examples/next.js/pages/index.jsx similarity index 100% rename from example/pages/index.jsx rename to examples/next.js/pages/index.jsx diff --git a/example/pages/login.jsx b/examples/next.js/pages/login.jsx similarity index 100% rename from example/pages/login.jsx rename to examples/next.js/pages/login.jsx diff --git a/example/pages/profile-sg.jsx b/examples/next.js/pages/profile-sg.jsx similarity index 100% rename from example/pages/profile-sg.jsx rename to examples/next.js/pages/profile-sg.jsx diff --git a/example/pages/profile-ssr.jsx b/examples/next.js/pages/profile-ssr.jsx similarity index 100% rename from example/pages/profile-ssr.jsx rename to examples/next.js/pages/profile-ssr.jsx diff --git a/example/yarn.lock b/examples/next.js/yarn.lock similarity index 100% rename from example/yarn.lock rename to examples/next.js/yarn.lock diff --git a/lib/index.js b/lib/index.js index 1a86cf38..4466d9ac 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,8 @@ import ironStore from "iron-store"; import cookie from "cookie"; +import createDebug from "debug"; + +const debug = createDebug("next-iron-session"); // default time allowed to check for iron seal validity when ttl passed // see https://hapi.dev/family/iron/api/?v=6.0.0#options @@ -9,71 +12,106 @@ function throwOnNoPassword() { throw new Error("next-iron-session: Missing parameter `password`"); } +function throwOnNoCookieName() { + throw new Error("next-iron-session: Missing parameter `cookieName`"); +} + function computeCookieMaxAge(ttl) { // The next line makes sure browser will expire cookies before seals are considered expired by the server. // It also allows for clock difference of 60 seconds maximum between server and clients. return (ttl === 0 ? 2147483647 : ttl) - timestampSkewSec; } -const defaultCookieOptions = { - httpOnly: true, - secure: true, - sameSite: "lax", - path: "/", -}; +function getCookieOptions({ userCookieOptions, ttl }) { + const defaultCookieOptions = { + httpOnly: true, + secure: true, + sameSite: "lax", + path: "/", + }; -export default function withIronSession( - withIronSessionWrapperHandler, + return { + ...defaultCookieOptions, + ...userCookieOptions, + maxAge: userCookieOptions.maxAge || computeCookieMaxAge(ttl), + }; +} + +export async function applySession( + req, + res, { ttl = 15 * 24 * 3600, - cookieName = "__ironSession", + cookieName = throwOnNoCookieName(), password = throwOnNoPassword(), cookieOptions: userCookieOptions = {}, - } = {}, + }, ) { - const cookieOptions = { - ...defaultCookieOptions, - ...userCookieOptions, - maxAge: userCookieOptions.maxAge || computeCookieMaxAge(ttl), + const cookieOptions = getCookieOptions({ userCookieOptions, ttl }); + + const store = await getOrCreateStore({ + sealed: cookie.parse(req.headers.cookie || "")[cookieName], + password, + ttl: ttl * 1000, + }); + + const session = { + set: store.set, + get: store.get, + unset: store.unset, + async save() { + const seal = await store.seal(); + const cookieValue = cookie.serialize(cookieName, seal, cookieOptions); + res.setHeader("set-cookie", [cookieValue]); + }, + destroy() { + store.clear(); + const cookieValue = cookie.serialize(cookieName, "", { + ...cookieOptions, + maxAge: 0, + }); + res.setHeader("set-cookie", [cookieValue]); + }, }; + req.session = session; +} + +export function withIronSession( + withIronSessionWrapperHandler, + { + ttl = 15 * 24 * 3600, + cookieName = throwOnNoCookieName(), + password = throwOnNoPassword(), + cookieOptions = {}, + }, +) { return async function withIronSessionHandler(...args) { const handlerType = args[0] && args[1] ? "api" : "ssr"; const req = handlerType === "api" ? args[0] : args[0].req; const res = handlerType === "api" ? args[1] : args[0].res; - const store = await getOrCreateStore({ - sealed: cookie.parse(req.headers.cookie || "")[cookieName], - password, - ttl: ttl * 1000, - }); - - const session = { - set: store.set, - get: store.get, - setFlash: store.setFlash, - unset: store.unset, - async save() { - const seal = await store.seal(); - const cookieValue = cookie.serialize(cookieName, seal, cookieOptions); - res.setHeader("set-cookie", [cookieValue]); - }, - destroy() { - store.clear(); - const cookieValue = cookie.serialize(cookieName, "", { - ...cookieOptions, - maxAge: 0, - }); - res.setHeader("set-cookie", [cookieValue]); - }, - }; - - req.session = session; + await applySession(req, res, { ttl, cookieName, password, cookieOptions }); return withIronSessionWrapperHandler(...args); }; } +export function ironSession({ + ttl = 15 * 24 * 3600, + cookieName = throwOnNoCookieName(), + password = throwOnNoPassword(), + cookieOptions = {}, +}) { + return function (req, res, next) { + applySession(req, res, { ttl, cookieName, password, cookieOptions }) + .then(() => { + next(); + }) + .catch(next); + }; +} + async function getOrCreateStore({ sealed, password, ttl }) { try { return await ironStore({ @@ -87,6 +125,10 @@ async function getOrCreateStore({ sealed, password, ttl }) { err.message === "Bad hmac value" || err.message === "Cannot find password: " ) { + debug( + "Received error from Iron: %s, session was automatically restarted", + err.message, + ); // if seal expires or // if seal is not valid (encrypted using a different password, when passwords are updated) or // if we can't find back the password in the seal diff --git a/lib/index.test.js b/lib/index.test.js index fd3b7121..c17bc303 100644 --- a/lib/index.test.js +++ b/lib/index.test.js @@ -1,15 +1,16 @@ import { advanceBy, clear } from "jest-date-mock"; import ironStore from "iron-store"; -import withIronSession from "./index.js"; +import { withIronSession, ironSession, applySession } from "./index.js"; const password = "Gbm49ATjnqnkCCCdhV4uDBhbfnPqsCW0"; +const cookieName = "test"; -test("withSession(handler) without a password", () => { +test("without a password", () => { return new Promise((done) => { const handler = () => {}; expect(() => { - withIronSession(handler); + withIronSession(handler, { cookieName }); }).toThrowErrorMatchingInlineSnapshot( `"next-iron-session: Missing parameter \`password\`"`, ); @@ -17,7 +18,19 @@ test("withSession(handler) without a password", () => { }); }); -test("withSession((req, res) => {}, {password})", () => { +test("without a cookieName", () => { + return new Promise((done) => { + const handler = () => {}; + expect(() => { + withIronSession(handler, { password }); + }).toThrowErrorMatchingInlineSnapshot( + `"next-iron-session: Missing parameter \`cookieName\`"`, + ); + done(); + }); +}); + +test("withSession((req, res) => {}, {password, cookieName})", () => { return new Promise((done) => { const handler = (req, res) => { expect(req).toMatchInlineSnapshot(` @@ -30,7 +43,6 @@ test("withSession((req, res) => {}, {password})", () => { "get": [Function], "save": [Function], "set": [Function], - "setFlash": [Function], "unset": [Function], }, } @@ -42,7 +54,7 @@ test("withSession((req, res) => {}, {password})", () => { `); done(); }; - const wrappedHandler = withIronSession(handler, { password }); + const wrappedHandler = withIronSession(handler, { password, cookieName }); wrappedHandler( { headers: { cookie: "sg=1" }, @@ -52,7 +64,7 @@ test("withSession((req, res) => {}, {password})", () => { }); }); -test("withSession(({req, res}) => {}, {password})", () => { +test("withSession(({req, res}) => {}, {password, cookieName})", () => { return new Promise((done) => { const handler = ({ req, res }) => { expect(req).toMatchInlineSnapshot(` @@ -65,7 +77,6 @@ test("withSession(({req, res}) => {}, {password})", () => { "get": [Function], "save": [Function], "set": [Function], - "setFlash": [Function], "unset": [Function], }, } @@ -77,7 +88,7 @@ test("withSession(({req, res}) => {}, {password})", () => { `); done(); }; - const wrappedHandler = withIronSession(handler, { password }); + const wrappedHandler = withIronSession(handler, { password, cookieName }); wrappedHandler({ req: { headers: { cookie: "ssr=1" } }, res: { json: function () {} }, @@ -95,27 +106,7 @@ test("req.session.set", () => { `); done(); }; - const wrappedHandler = withIronSession(handler, { password }); - wrappedHandler( - { - headers: { cookie: "" }, - }, - {}, - ); - }); -}); - -test("req.session.setFlash", () => { - return new Promise((done) => { - const handler = (req) => { - req.session.setFlash("state", "dfsafsalfk21lkf12lkf21"); - expect(req.session.get("state")).toMatchInlineSnapshot( - `"dfsafsalfk21lkf12lkf21"`, - ); - expect(req.session.get("state")).toMatchInlineSnapshot(`undefined`); - done(); - }; - const wrappedHandler = withIronSession(handler, { password }); + const wrappedHandler = withIronSession(handler, { password, cookieName }); wrappedHandler( { headers: { cookie: "" }, @@ -138,7 +129,7 @@ test("req.session.unset", () => { expect(req.session.get("state")).toMatchInlineSnapshot(`undefined`); done(); }; - const wrappedHandler = withIronSession(handler, { password }); + const wrappedHandler = withIronSession(handler, { password, cookieName }); wrappedHandler( { headers: { cookie: "" }, @@ -169,7 +160,7 @@ test("req.session.save creates a seal and stores it in a cookie", () => { ); done(); }; - const wrappedHandler = withIronSession(handler, { password }); + const wrappedHandler = withIronSession(handler, { password, cookieName }); wrappedHandler( { headers: { cookie: "" }, @@ -193,12 +184,12 @@ test("withSession((req, res) => {}, {password}) with existing session (SG)", asy `); done(); }; - const wrappedHandler = withIronSession(handler, { password }); + const wrappedHandler = withIronSession(handler, { password, cookieName }); wrappedHandler( { headers: { cookie: - "__ironSession=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw", + "test=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw", }, }, {}, @@ -218,12 +209,12 @@ test("withSession(({req, res}) => {}, {password}) with existing session (SSR)", `); done(); }; - const wrappedHandler = withIronSession(handler, { password }); + const wrappedHandler = withIronSession(handler, { password, cookieName }); wrappedHandler({ req: { headers: { cookie: - "__ironSession=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw", + "test=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw", }, }, }); @@ -240,7 +231,11 @@ test("When ttl is 0, maxAge have a specific value", () => { expect(maxAgeParam).toMatchInlineSnapshot(`" Max-Age=2147483587"`); done(); }; - const wrappedHandler = withIronSession(handler, { password, ttl: 0 }); + const wrappedHandler = withIronSession(handler, { + password, + cookieName, + ttl: 0, + }); wrappedHandler( { headers: { cookie: "" }, @@ -262,13 +257,17 @@ test("req.session.destroy", () => { Array [ "set-cookie", Array [ - "__ironSession=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax", + "test=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Lax", ], ] `); done(); }; - const wrappedHandler = withIronSession(handler, { password, ttl: 0 }); + const wrappedHandler = withIronSession(handler, { + password, + cookieName, + ttl: 0, + }); wrappedHandler( { headers: { cookie: "coucou=true" }, @@ -298,11 +297,15 @@ test("When trying to use an expired seal", async () => { `); done(); }; - const wrappedHandler = withIronSession(handler, { password, ttl }); + const wrappedHandler = withIronSession(handler, { + password, + cookieName, + ttl, + }); wrappedHandler( { - headers: { cookie: `__ironSession=${seal}` }, + headers: { cookie: `test=${seal}` }, }, { setHeader: jest.fn(), @@ -319,10 +322,14 @@ test("When trying to use an expired seal", async () => { expect(req.session.get("user")).toMatchInlineSnapshot(`undefined`); done(); }; - const wrappedHandler = withIronSession(handler, { password, ttl }); + const wrappedHandler = withIronSession(handler, { + password, + cookieName, + ttl, + }); wrappedHandler( { - headers: { cookie: `__ironSession=${seal}` }, + headers: { cookie: `test=${seal}` }, }, { setHeader: jest.fn(), @@ -344,6 +351,7 @@ test("It throws Iron errors when passing a wrong password (password length must }; const wrappedHandler = withIronSession(handler, { password: "dsadsadsadsadsadadsa", + cookieName, ttl: 0, }); await expect( @@ -351,7 +359,7 @@ test("It throws Iron errors when passing a wrong password (password length must { headers: { cookie: - "__ironSession=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw", + "test=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw", }, }, { @@ -373,7 +381,7 @@ test("when no cookies at all", () => { `); done(); }; - const wrappedHandler = withIronSession(handler, { password }); + const wrappedHandler = withIronSession(handler, { password, cookieName }); wrappedHandler( { headers: {}, @@ -398,10 +406,11 @@ test("When trying to use a wrong seal (example: password was updated server-side }; const wrappedHandler = withIronSession(handler, { password: secondPassword, + cookieName, }); wrappedHandler( { - headers: { cookie: `__ironSession=${seal}` }, + headers: { cookie: `test=${seal}` }, }, {}, ); @@ -416,12 +425,13 @@ test("moving from <=3.1.2 seals to multi passwords creates a new session", async }; const wrappedHandler = withIronSession(handler, { password: [{ id: 1, password }], + cookieName, }); wrappedHandler( { headers: { cookie: - "__ironSession=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw", + "test=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw", }, }, {}, @@ -453,12 +463,136 @@ test("Password rotation", async () => { }; const wrappedHandler = withIronSession(handler, { password: secondPassword, + cookieName, }); wrappedHandler( { - headers: { cookie: `__ironSession=${seal}` }, + headers: { cookie: `test=${seal}` }, }, {}, ); }); }); + +test("Connect middleware ironSession({password, cookieName})", () => { + return new Promise((done) => { + const req = { + headers: { cookie: "sg=1" }, + }; + const res = { json: function () {} }; + + const handler = ironSession({ password, cookieName }); + handler(req, res, function () { + expect(req).toMatchInlineSnapshot(` + Object { + "headers": Object { + "cookie": "sg=1", + }, + "session": Object { + "destroy": [Function], + "get": [Function], + "save": [Function], + "set": [Function], + "unset": [Function], + }, + } + `); + expect(res).toMatchInlineSnapshot(` + Object { + "json": [Function], + } + `); + done(); + }); + }); +}); + +test("Express middleware with error", () => { + return new Promise((done) => { + const req = { + headers: { + cookie: + "test=Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw", + }, + }; + const res = {}; + + const handler = ironSession({ + password: "wrong password length", + cookieName, + }); + handler(req, res, function (err) { + expect(err).toMatchInlineSnapshot( + `[Error: Password string too short (min 32 characters required)]`, + ); + done(); + }); + }); +}); + +test("applySession(req, res, {password, cookieName})", async () => { + const req = { + headers: { cookie: "sg=1" }, + }; + const res = { json: function () {} }; + + await applySession(req, res, { password, cookieName }); + + expect(req).toMatchInlineSnapshot(` + Object { + "headers": Object { + "cookie": "sg=1", + }, + "session": Object { + "destroy": [Function], + "get": [Function], + "save": [Function], + "set": [Function], + "unset": [Function], + }, + } + `); + expect(res).toMatchInlineSnapshot(` + Object { + "json": [Function], + } + `); +}); + +test("applySession(req, res, {cookieName})", async () => { + const req = {}; + const res = {}; + + await expect(async function () { + await applySession(req, res, { cookieName }); + }).rejects.toThrowErrorMatchingInlineSnapshot( + `"next-iron-session: Missing parameter \`password\`"`, + ); +}); + +test("applySession(req, res, {password})", async () => { + const req = {}; + const res = {}; + + await expect(async function () { + await applySession(req, res, { password }); + }).rejects.toThrowErrorMatchingInlineSnapshot( + `"next-iron-session: Missing parameter \`cookieName\`"`, + ); +}); + +test("ironSession({cookieName})", () => { + expect(function () { + ironSession({ cookieName }); + }).toThrowErrorMatchingInlineSnapshot( + `"next-iron-session: Missing parameter \`password\`"`, + ); +}); + +test("ironSession({password})", () => { + expect(function () { + ironSession({ password }); + }).toThrowErrorMatchingInlineSnapshot( + `"next-iron-session: Missing parameter \`cookieName\`"`, + ); +}); diff --git a/package.json b/package.json index ccfe7f48..864d1596 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "dependencies": { "clone": "^2.1.2", "cookie": "^0.4.0", + "debug": "^4.1.1", "iron-store": "^1.3.0" }, "devDependencies": {