diff --git a/.gitignore b/.gitignore index badecbf..6bf20e9 100755 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ # npm node_modules/ +npm-debug.log # Docker Dockerfile +# Istanbul reports +coverage/ + # Secret stuff config/env/* !config/env/sample.js @@ -16,39 +20,3 @@ static/**/*.bun.* # Ignore docs files _gh_pages _site - -# Numerous always-ignore extensions -*.diff -*.err -*.orig -*.log -*.rej -*.swo -*.swp -*.zip -*.vi -*~ - -# OS or Editor folders -.DS_Store -._* -Thumbs.db -.cache -.project -.settings -.tmproj -*.esproj -nbproject -*.sublime-project -*.sublime-workspace -.idea -.c9 -c9d - -# Komodo -*.komodoproject -.komodotools - -# grunt-html-validation -validation-status.json -validation-report.json diff --git a/.travis.yml b/.travis.yml index 3385e77..6a6d197 100755 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,9 @@ branches: only: - master build: - echo "module.exports = require('./travis.js');" > config/env/env.js \ No newline at end of file + - echo "module.exports = require('./travis.js')" > config/env/env.js +script: + - npm run cover + +# Send coverage data to Coveralls +after_script: "cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 23427ec..647bc87 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Tracman Server Changelog -### v0.8.2 +### v0.9.0 + +###### v0.9.0 +* [#121](https://github.com/Tracman-org/Server/issues/121) Fixed various security holes +* [#68](https://github.com/Tracman-org/Server/issues/68) Added tests, mostly for authentication +* [#120](https://github.com/Tracman-org/Server/issues/120) Split config/routes/settings.js into two files +* Removed express validator and replaced with homegrown function +* Fixed showing welcome message on every login +* Removed naked domains ###### v0.8.1/2 * Hotfixed service worker bugs diff --git a/README.md b/README.md index 384011f..b1939dd 100755 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # TTracman -###### v 0.8.2 +###### v 0.9.0 node.js application to display a sharable map with user's location. -[![Build Status](https://travis-ci.org/Tracman-org/Server.svg?branch=develop)](https://travis-ci.org/Tracman-org/Server) +[![Travis Build Status](https://travis-ci.org/Tracman-org/Server.svg?branch=develop)](https://travis-ci.org/Tracman-org/Server) +[![Coverage Status](https://coveralls.io/repos/github/Tracman-org/Server/badge.svg?branch=master)](https://coveralls.io/github/Tracman-org/Server?branch=master) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) -[![Waffle.io - Columns and their card count](https://badge.waffle.io/Tracman-org/Server.svg?columns=all)](https://waffle.io/Tracman-org/Server) +[![Snyk Vulnerabilities](https://snyk.io/test/github/Tracman-org/Server/badge.svg)](https://snyk.io/test/github/Tracman-org/Servr) ## Installation @@ -23,7 +24,7 @@ A good method is to simply copy the sample configuration and point `config/env/e ```sh cp config/env/sample.js config/env/local-config.js -echo "module.exports = require('./local-config.js');" > config/env/env.js +echo "module.exports = require('./local-config.js')" > config/env/env.js ``` Then edit `config/env/local-config.js` to match your local environment. @@ -55,10 +56,16 @@ Tracman will be updated according to [this branching model](http://nvie.com/post [view full changelog](CHANGELOG.md) -###### v0.8.1/2 -* Hotfixed service worker bugs +###### v0.9.0 +* [#121](https://github.com/Tracman-org/Server/issues/121) Fixed various security holes +* [#68](https://github.com/Tracman-org/Server/issues/68) Added tests, mostly for authentication +* [#120](https://github.com/Tracman-org/Server/issues/120) Split config/routes/settings.js into two files +* Removed express validator and replaced with homegrown function +* Fixed showing welcome message on every login +* Removed naked domains -#### v0.8.0 +###### v0.8.x +* Hotfixed service worker bugs * Added check to ensure only the newest location is sent * Removed buggy login/-out redirects * [#111](https://github.com/Tracman-org/Server/issues/111) Implemented service worker @@ -93,7 +100,7 @@ Tracman will be updated according to [this branching model](http://nvie.com/post [view full license](LICENSE.md) Tracman: GPS tracking service in node.js -Copyright © 2017 [Keith Irwin](https://keithirwin.us/) +Copyright © 2018 [Keith Irwin](https://www.keithirwin.us/) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/config/models.js b/config/models.js index 4d9795b..57dfe74 100755 --- a/config/models.js +++ b/config/models.js @@ -24,6 +24,7 @@ const userSchema = new mongoose.Schema({ isPro: {type: Boolean, required: true, default: false}, created: {type: Date, required: true}, lastLogin: Date, + isNewUser: Boolean, settings: { units: {type: String, default: 'standard'}, defaultMap: {type: String, default: 'road'}, @@ -78,14 +79,10 @@ userSchema.methods.createPassToken = function () { return new Promise( async (resolve, reject) => { - // Reuse old token, resetting clock + // Reuse old token if (user.auth.passTokenExpires >= Date.now()) { debug(`Reusing old password token...`) - user.auth.passTokenExpires = Date.now() + 3600000 // 1 hour - try { - await user.save() - resolve([user.auth.passToken, user.auth.passTokenExpires]) - } catch (err) { reject(err) } + resolve([user.auth.passToken, user.auth.passTokenExpires]) // Create new token } else { diff --git a/config/passport.js b/config/passport.js index e8269b4..5379446 100755 --- a/config/passport.js +++ b/config/passport.js @@ -7,6 +7,7 @@ const TwitterStrategy = require('passport-twitter').Strategy const GoogleTokenStrategy = require('passport-google-id-token') const FacebookTokenStrategy = require('passport-facebook-token') const TwitterTokenStrategy = require('passport-twitter-token') +const sanitize = require('mongo-sanitize') const debug = require('debug')('tracman-passport') const env = require('./env/env.js') const mw = require('./middleware.js') @@ -33,7 +34,7 @@ module.exports = (passport) => { }, async (req, email, password, done) => { debug(`Perfoming local login for ${email}`) try { - let user = await User.findOne({'email': email}) + let user = await User.findOne({'email': sanitize(email)}) // No user with that email if (!user) { @@ -45,16 +46,16 @@ module.exports = (passport) => { debug(`User exists. Checking password...`) // Check password - let res = await user.validPassword(password) + let correct_password = await user.validPassword(password) // Password incorrect - if (!res) { + if (!correct_password) { debug(`Incorrect password`) return done(null, false, req.flash('warning', 'Incorrect email or password.')) // Successful login } else { - if (!user.lastLogin) req.forNewUser = true + user.isNewUser = !Boolean(user.lastLogin) user.lastLogin = Date.now() user.save() return done(null, user) @@ -143,11 +144,11 @@ module.exports = (passport) => { // Check for unique profileId debug(`Checking for unique account with query ${query}...`) try { - let user = await User.findOne(query) + let existing_user = await User.findOne(query) // Social account already in use - if (existingUser) { - debug(`${service} account already in use with user ${existingUser.id}`) + if (existing_user) { + debug(`${service} account already in use with user ${existing_user.id}`) req.session.flashType = 'warning' req.session.flashMessage = `Another user is already connected to that ${service} account. ` return done() diff --git a/config/routes/account.js b/config/routes/account.js new file mode 100644 index 0000000..e2b73b1 --- /dev/null +++ b/config/routes/account.js @@ -0,0 +1,169 @@ +'use strict' + +const mw = require('../middleware.js') +const sanitize = require('mongo-sanitize') +const User = require('../models.js').user +const mail = require('../mail.js') +const env = require('../env/env.js') +const zxcvbn = require('zxcvbn') +const moment = require('moment') +const debug = require('debug')('tracman-routes-account') +const router = require('express').Router() + + +// Confirm email address +router.get('/email/:token', mw.ensureAuth, async (req, res, next) => { + // Check token + if (req.user.emailToken === req.params.token) { + try { + // Set new email + req.user.email = req.user.newEmail + + // Delete token and newEmail + req.user.emailToken = undefined + req.user.newEmail = undefined + + await req.user.save() + + // Report success + req.flash('success', `Your email has been set to ${req.user.email}. `) + res.redirect('/settings') + + } catch (err) { + mw.throwErr(err, req) + res.redirect('/settings') + } + + // Invalid token + } else { + req.flash('danger', 'Email confirmation token is invalid. ') + res.redirect('/settings') + } +}) + +// Set password +router.route('/password') + .all(mw.ensureAuth, (req, res, next) => { + next() + }) + + // Email user a token, proceed at /password/:token + .get( async (req, res, next) => { + // Create token for password change + try { + let [token, expires] = await req.user.createPassToken() + // Figure out expiration time + let expirationTimeString = (req.query.tz) + ? moment(expires).utcOffset(req.query.tz).toDate().toLocaleTimeString(req.acceptsLanguages[0]) + : moment(expires).toDate().toLocaleTimeString(req.acceptsLanguages[0]) + ' UTC' + + // Alert user to check email. + req.flash('success', + `An link has been sent to ${req.user.email}. \ + Click on the link to complete your password change. \ + This link will expire in one hour (${expirationTimeString}). ` + ) + + // Confirm password change request by email. + return await mail.send({ + to: mail.to(req.user), + from: mail.noReply, + subject: 'Request to change your Tracman password', + text: mail.text( + `A request has been made to change your tracman password. \ + If you did not initiate this request, please contact support at keith@tracman.org. \ + \n\nTo change your password, follow this link:\n\ + ${env.url}/account/password/${token}. \n\n\ + This request will expire at ${expirationTimeString}. ` + ), + html: mail.html( + `

A request has been made to change your tracman password. \ + If you did not initiate this request, please contact support at \ + keith@tracman.org.

\ +

To change your password, follow this link:\ +
\ + ${env.url}/account/password/${token}.

\ +

This request will expire at ${expirationTimeString}.

` + ) + }) + + } catch (err) { + mw.throwErr(err, req) + } finally { + res.redirect((req.user) ? '/settings' : '/login') + } + }) + +router.route('/password/:token') + + // Check token + .all( async (req, res, next) => { + debug('/account/password/:token .all() called') + try { + let user = await User + .findOne({'auth.passToken': sanitize(req.params.token)}) + .where('auth.passTokenExpires').gt(Date.now()) + + if (!user) { + debug('Bad token') + req.flash('danger', 'Password reset token is invalid or has expired. ') + res.redirect((req.isAuthenticated) ? '/settings' : '/login') + } else { + debug('setting passwordUser') + res.locals.passwordUser = user + next() + } + + } catch (err) { + mw.throwErr(err, req) + res.redirect('/password') + } + + }) + + // Show password change form + .get((req, res) => { + debug('/account/password/:token .get() called') + res.render('password') + }) + + // Set new password + .post( async (req, res, next) => { + debug('/account/password/:token .post() called') + + // Validate password strength + let zxcvbnResult = zxcvbn(req.body.password) + if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 864000) { // Less than ten days + req.flash( 'danger', + `That password could be cracked in ${zxcvbnResult.crack_times_display.online_no_throttling_10_per_second}! Come up with a more complex password that would take at least 10 days to crack. ` + ) + res.redirect(`/account/password/${req.params.token}`) + } else { + + // Create hashed password and save to db + try { + await res.locals.passwordUser.generateHashedPassword(req.body.password) + + // User changed password + if (req.user) { + debug('User saved password') + req.flash('success', 'Your password has been changed. ') + res.redirect('/settings') + + // New user created password + } else { + debug('New user created password') + req.flash('success', 'Password set. You can use it to log in now. ') + res.redirect('/login') + } + + } catch (err) { + debug('Error creating hashed password and saving to db') + mw.throwErr(err, req) + res.redirect(`/account/password/${req.params.token}`) + } + + } + }) + +module.exports = router diff --git a/config/routes/auth.js b/config/routes/auth.js index be925cd..4c97ea0 100755 --- a/config/routes/auth.js +++ b/config/routes/auth.js @@ -6,6 +6,7 @@ const User = require('../models.js').user const crypto = require('crypto') const moment = require('moment') const slugify = require('slug') +const sanitize = require('mongo-sanitize') const debug = require('debug')('tracman-routes-auth') const env = require('../env/env.js') @@ -21,7 +22,7 @@ module.exports = (app, passport) => { req.flash(req.session.flashType, req.session.flashMessage) req.session.flashType = undefined req.session.flashMessage = undefined - res.redirect('/map'+(req.forNewUser)?'/map?new=1':'') + res.redirect('/map') } const appLoginCallback = (req, res, next) => { debug('appLoginCallback called.') @@ -57,7 +58,7 @@ module.exports = (app, passport) => { .post( async (req, res, next) => { // Send token and alert user - async function sendToken(user) { + const sendToken = async function(user) { debug(`sendToken() called for user ${user.id}`) // Create a new password token @@ -80,14 +81,14 @@ module.exports = (app, passport) => { subject: 'Complete your Tracman registration', text: mail.text( `Welcome to Tracman! \n\nTo complete your registration, follow \ - this link and set your password:\n${env.url}/settings/password/${token}\n\n\ + this link and set your password:\n${env.url}/account/password/${token}\n\n\ This link will expire at ${expiration_time_string}. ` ), html: mail.html( `

Welcome to Tracman!

To complete your registration, \ follow this link and set your password:\ -
\ - ${env.url}/settings/password/${token}

\ +
\ + ${env.url}/account/password/${token}

\

This link will expire at ${expiration_time_string}.

` ) }) @@ -131,114 +132,124 @@ module.exports = (app, passport) => { } - // Validate email - req.checkBody('email', 'Please enter a valid email address.').isEmail() - - // Check if somebody already has that email - try { - debug(`Searching for user with email ${req.body.email}...`) - let user = await User.findOne({'email': req.body.email}) - - // User already exists - if (user && user.auth.password) { - debug(`User ${user.id} has email ${req.body.email} and has a password`) - req.flash('warning', - `A user with that email already exists! If you forgot your password, \ - you can reset it here.` - ) - res.redirect('/login#login') - next() - - // User exists but hasn't created a password yet - } else if (user) { - debug(`User ${user.id} has email ${req.body.email} but doesn't have a password`) - - // Send another token - sendToken(user) - - // Create user - } else { - debug(`User with email ${req.body.email} doesn't exist; creating one`) - - let email = req.body.email - - user = new User() - user.created = Date.now() - user.email = email - user.slug = slugify(email.substring(0, email.indexOf('@'))) - - // Generate unique slug - const slug = new Promise((resolve, reject) => { - debug(`Creating new slug for user...`); - - (async function checkSlug (s, cb) { - try { - debug(`Checking to see if slug ${s} is taken...`) - let existingUser = await User.findOne({slug: s}) - - // Slug in use: generate a random one and retry - if (existingUser) { - debug(`Slug ${s} is taken; generating another...`) - crypto.randomBytes(6, (err, buf) => { - if (err) { - debug('Failed to create random bytes for slug!') - mw.throwErr(err, req) - reject() - } - if (buf) { - checkSlug(buf.toString('hex'), cb) - } - }) - - // Unique slug: proceed - } else { - debug(`Slug ${s} is unique`) - cb(s) - } - } catch (err) { - debug('Failed to create slug!') - mw.throwErr(err, req) - reject() - } - - })(user.slug, (newSlug) => { - debug(`Successfully created slug: ${newSlug}`) - user.slug = newSlug - resolve() - }) - }) - - // Generate sk32 - const sk32 = new Promise((resolve, reject) => { - debug('Creating sk32 for user...') - crypto.randomBytes(32, (err, buf) => { - if (err) { - debug('Failed to create sk32!') - mw.throwErr(err, req) - reject() - } - if (buf) { - user.sk32 = buf.toString('hex') - debug(`Successfully created sk32: ${user.sk32}`) - resolve() - } - }) - }) - - // Save user and send the token by email - try { - await Promise.all([slug, sk32]) - sendToken(user) - } catch (err) { - debug('Failed to save user after creating slug and sk32!') - mw.throwErr(err, req) - res.redirect('/login#signup') - } - } - } catch (err) { - debug(`Failed to check if somebody already has the email ${req.body.email}`) - mw.throwErr(err, req) + // Invalid email + if (!mw.validateEmail(req.body.email)) { + debug(`Email ${req.body.email} was found invalid!`) + req.flash('warning', `The email you entered, ${req.body.email} isn't valid. Try again. `) res.redirect('/login#signup') + next() + + // Valid email + } else { + debug(`Email ${req.body.email} was found valid.`) + + // Check if somebody already has that email + try { + debug(`Searching for user with email ${req.body.email}...`) + let user = await User.findOne({'email': sanitize(req.body.email)}) + + // User already exists + if (user && user.auth.password) { + debug(`User ${user.id} has email ${req.body.email} and has a password`) + req.flash('warning', + `A user with that email already exists! If you forgot your password, \ + you can reset it here.` + ) + res.redirect('/login#login') + next() + + // User exists but hasn't created a password yet + } else if (user) { + debug(`User ${user.id} has email ${req.body.email} but doesn't have a password`) + + // Send another token + sendToken(user) + + // Create user + } else { + debug(`User with email ${req.body.email} doesn't exist; creating one`) + + let email = req.body.email + + user = new User() + user.created = Date.now() + user.email = email + user.slug = slugify(email.substring(0, email.indexOf('@'))) + + // Generate unique slug + const slug = new Promise((resolve, reject) => { + debug(`Creating new slug for user...`); + + (async function checkSlug (s, cb) { + try { + debug(`Checking to see if slug ${s} is taken...`) + let existingUser = await User.findOne({slug: sanitize(s)}) + + // Slug in use: generate a random one and retry + if (existingUser) { + debug(`Slug ${s} is taken; generating another...`) + crypto.randomBytes(6, (err, buf) => { + if (err) { + debug('Failed to create random bytes for slug!') + mw.throwErr(err, req) + reject() + } + if (buf) { + checkSlug(buf.toString('hex'), cb) + } + }) + + // Unique slug: proceed + } else { + debug(`Slug ${s} is unique`) + cb(s) + } + } catch (err) { + debug('Failed to create slug!') + mw.throwErr(err, req) + reject() + } + + })(user.slug, (newSlug) => { + debug(`Successfully created slug: ${newSlug}`) + user.slug = newSlug + resolve() + }) + }) + + // Generate sk32 + const sk32 = new Promise((resolve, reject) => { + debug('Creating sk32 for user...') + crypto.randomBytes(32, (err, buf) => { + if (err) { + debug('Failed to create sk32!') + mw.throwErr(err, req) + reject() + } + if (buf) { + user.sk32 = buf.toString('hex') + debug(`Successfully created sk32: ${user.sk32}`) + resolve() + } + }) + }) + + // Save user and send the token by email + try { + await Promise.all([slug, sk32]) + sendToken(user) + } catch (err) { + debug('Failed to save user after creating slug and sk32!') + mw.throwErr(err, req) + res.redirect('/login#signup') + } + } + } catch (err) { + debug(`Failed to check if somebody already has the email ${req.body.email}`) + mw.throwErr(err, req) + res.redirect('/login#signup') + } + } }) @@ -259,65 +270,87 @@ module.exports = (app, passport) => { // Submitted forgot password form .post( async (req, res, next) => { - // Validate email - req.checkBody('email', 'Please enter a valid email address.').isEmail() - // Check if somebody has that email - try { - let user = await User.findOne({'email': req.body.email}) - - // No user with that email - if (!user) { - // Don't let on that no such user exists, to prevent dictionary attacks - req.flash('success', - `If an account exists with the email ${req.body.email}, \ - an email has been sent there with a password reset link. ` - ) - res.redirect('/login') - - // User with that email does exist - } else { - - // Create reset token - try { - let [token, expires] = await user.createPassToken() - - // Email reset link - try { - await mail.send({ - from: mail.noReply, - to: mail.to(user), - subject: 'Reset your Tracman password', - text: mail.text( - `Hi, \n\nDid you request to reset your Tracman password? \ - If so, follow this link to do so:\ - \n${env.url}/settings/password/${token}\n\n\ - If you didn't initiate this request, just ignore this email. ` - ), - html: mail.html( - `

Hi,

Did you request to reset your Tracman password? \ - If so, follow this link to do so:
\ - \ - ${env.url}/settings/password/${token}

\ -

If you didn't initiate this request, just ignore this email.

` - ) - }) - req.flash( - 'success', - `If an account exists with the email ${req.body.email}, \ - an email has been sent there with a password reset link. `) - res.redirect('/login') - } catch (err) { - debug(`Failed to send reset link to ${user.email}`) - mw.throwErr(err, req) - res.redirect('/login') - } - } catch (err) { return next(err) } - } - } catch (err) { - debug(`Failed to check for if somebody has that email (in reset request)!`) - mw.throwErr(err, req) + // Invalid email + if (!mw.validateEmail(req.body.email)) { + debug(`Email ${req.body.email} was found invalid!`) + req.flash('warning', `The email you entered, ${req.body.email} isn't valid. Try again. `) res.redirect('/login/forgot') + next() + + // Valid email + } else { + debug(`Email ${req.body.email} was found valid.`) + + // Check if somebody has that email + try { + let user = await User.findOne({'email': sanitize(req.body.email)}) + + // No user with that email + if (!user) { + debug(`No user found with email ${req.body.email}; ignoring password request.`) + // Don't let on that no such user exists, to prevent dictionary attacks + req.flash('success', + `If an account exists with the email ${req.body.email}, \ + an email has been sent there with a password reset link. ` + ) + res.redirect('/login') + + // User with that email does exist + } else { + debug(`User ${user.id} found with that email. Creating reset token...`) + + // Create reset token + try { + let [token, expires] = await user.createPassToken() + + // Figure out expiration time string + debug(`Determining expiration time string for ${expires}...`) + let expiration_time_string = (req.query.tz) + ? moment(expires).utcOffset(req.query.tz).toDate().toLocaleTimeString(req.acceptsLanguages[0]) + : moment(expires).toDate().toLocaleTimeString(req.acceptsLanguages[0]) + ' UTC' + + // Email reset link + try { + await mail.send({ + from: mail.noReply, + to: mail.to(user), + subject: 'Reset your Tracman password', + text: mail.text( + `Hi, \n\nDid you request to reset your Tracman password? \ + If so, follow this link to do so:\ + \n${env.url}/account/password/${token}\n\n\ + This link will expire at ${expiration_time_string}. \n\n\ + If you didn't initiate this request, just ignore this email. \n\n` + ), + html: mail.html( + `

Hi,

Did you request to reset your Tracman password? \ + If so, follow this link to do so:
\ + \ + ${env.url}/account/password/${token}. \ + This link will expire at ${expiration_time_string}.

\ +

If you didn't initiate this request, just ignore this email.

` + ) + }) + req.flash( + 'success', + `If an account exists with the email ${req.body.email}, \ + an email has been sent there with a password reset link.\ + (Your reset link will expire in one hour.)`) + res.redirect('/login') + } catch (err) { + debug(`Failed to send reset link to ${user.email}`) + mw.throwErr(err, req) + res.redirect('/login') + } + } catch (err) { return next(err) } + } + } catch (err) { + debug(`Failed to check for if somebody has that email (in reset request)!`) + mw.throwErr(err, req) + res.redirect('/login/forgot') + } + } }) @@ -355,7 +388,7 @@ module.exports = (app, passport) => { if (!req.user.auth.password && service === 'google') { req.flash( 'warning', - `Hey, you need to set a password \ + `Hey, you need to set a password \ before you can disconnect your google account. Otherwise, you \ won't be able to log in! ` ) @@ -377,4 +410,5 @@ module.exports = (app, passport) => { app.get('/login/google/cb', passport.authenticate('google', loginOutcome), loginCallback) app.get('/login/facebook/cb', passport.authenticate('facebook', loginOutcome), loginCallback) app.get('/login/twitter/cb', passport.authenticate('twitter', loginOutcome), loginCallback) + } diff --git a/config/routes/map.js b/config/routes/map.js index b38d279..10266fe 100755 --- a/config/routes/map.js +++ b/config/routes/map.js @@ -3,12 +3,12 @@ const router = require('express').Router() const mw = require('../middleware.js') const env = require('../env/env.js') +const sanitize = require('mongo-sanitize') const User = require('../models.js').user // Redirect to real slug router.get('/', mw.ensureAuth, (req, res) => { - if (req.query.new) res.redirect(`/map/${req.user.slug}?new=1`) - else res.redirect(`/map/${req.user.slug}`) + res.redirect(`/map/${req.user.slug}`) }) // Demo @@ -48,21 +48,25 @@ router.get('/demo', (req, res, next) => { // Show map router.get('/:slug?', async (req, res, next) => { try { - let map_user = await User.findOne({slug: req.params.slug}) - if (!map_user) next() // 404 - else { - var active = '' // For header nav - if (req.user && req.user.id === map_user.id) active = 'map' - res.render('map', { - active: active, - mapuser: map_user, - mapApi: env.googleMapsAPI, - user: req.user, - noFooter: '1', - noHeader: (req.query.noheader) ? req.query.noheader.match(/\d/)[0] : 0, - disp: (req.query.disp) ? req.query.disp.match(/\d/)[0] : 2, // 0=map, 1=streetview, 2=both - newuserurl: (req.query.new) ? env.url + '/map/' + req.params.slug : '' - }) + if (req.params.slug != sanitize(req.params.slug)) { + throw new Error(`Possible injection attempt with slug: ${req.params.slug}`) + } else { + let map_user = await User.findOne({slug: req.params.slug}) + if (!map_user) next() // 404 + else { + var active = '' // For header nav + if (req.user && req.user.id === map_user.id) active = 'map' + res.render('map', { + active: active, + mapuser: map_user, + mapApi: env.googleMapsAPI, + user: req.user, + noFooter: '1', + noHeader: (req.query.noheader) ? req.query.noheader.match(/\d/)[0] : 0, + disp: (req.query.disp) ? req.query.disp.match(/\d/)[0] : 2, // 0=map, 1=streetview, 2=both + newuserurl: (req.query.new) ? env.url + '/map/' + req.params.slug : '' + }) + } } } catch (err) { mw.throwErr(err, req) } }) diff --git a/config/routes/settings.js b/config/routes/settings.js index 89e3b3d..a8074a1 100755 --- a/config/routes/settings.js +++ b/config/routes/settings.js @@ -2,12 +2,11 @@ const slug = require('slug') const xss = require('xss') -const zxcvbn = require('zxcvbn') -const moment = require('moment') const mw = require('../middleware.js') const User = require('../models.js').user const mail = require('../mail.js') const env = require('../env/env.js') +const sanitize = require('mongo-sanitize') const debug = require('debug')('tracman-routes-settings') const router = require('express').Router() @@ -65,14 +64,14 @@ router.route('/') text: mail.text( `A request has been made to change your Tracman email address. \ If you did not initiate this request, please disregard it. \n\n\ - To confirm your email, follow this link:\n${env.url}/settings/email/${token}. ` + To confirm your email, follow this link:\n${env.url}/account/email/${token}. ` ), html: mail.html( `

A request has been made to change your Tracman email address. \ If you did not initiate this request, please disregard it.

\

To confirm your email, follow this link:\ -
\ - ${env.url}/settings/email/${token}.

` +
\ + ${env.url}/account/email/${token}.

` ) }) @@ -142,6 +141,7 @@ router.route('/') finally { res.redirect('/settings') } }) + // Delete account router.get('/delete', async (req, res) => { try { @@ -154,160 +154,6 @@ router.get('/delete', async (req, res) => { } }) -// Confirm email address -router.get('/email/:token', mw.ensureAuth, async (req, res, next) => { - // Check token - if (req.user.emailToken === req.params.token) { - try { - // Set new email - req.user.email = req.user.newEmail - - // Delete token and newEmail - req.user.emailToken = undefined - req.user.newEmail = undefined - - await req.user.save() - - // Report success - req.flash('success', `Your email has been set to ${req.user.email}. `) - res.redirect('/settings') - - } catch (err) { - mw.throwErr(err, req) - res.redirect('/settings') - } - - // Invalid token - } else { - req.flash('danger', 'Email confirmation token is invalid. ') - res.redirect('/settings') - } -}) - -// Set password -router.route('/password') - .all(mw.ensureAuth, (req, res, next) => { - next() - }) - - // Email user a token, proceed at /password/:token - .get( async (req, res, next) => { - // Create token for password change - try { - let [token, expires] = await req.user.createPassToken() - // Figure out expiration time - let expirationTimeString = (req.query.tz) - ? moment(expires).utcOffset(req.query.tz).toDate().toLocaleTimeString(req.acceptsLanguages[0]) - : moment(expires).toDate().toLocaleTimeString(req.acceptsLanguages[0]) + ' UTC' - - // Confirm password change request by email. - return await mail.send({ - to: mail.to(req.user), - from: mail.noReply, - subject: 'Request to change your Tracman password', - text: mail.text( - `A request has been made to change your tracman password. \ - If you did not initiate this request, please contact support at keith@tracman.org. \ - \n\nTo change your password, follow this link:\n\ - ${env.url}/settings/password/${token}. \n\n\ - This request will expire at ${expirationTimeString}. ` - ), - html: mail.html( - `

A request has been made to change your tracman password. \ - If you did not initiate this request, please contact support at \ - keith@tracman.org.

\ -

To change your password, follow this link:\ -
\ - ${env.url}/settings/password/${token}.

\ -

This request will expire at ${expirationTimeString}.

` - ) - }) - - // Alert user to check email. - req.flash('success', - `An link has been sent to ${req.user.email}. \ - Click on the link to complete your password change. \ - This link will expire in one hour (${expirationTimeString}). ` - ) - } catch (err) { - mw.throwErr(err, req) - } finally { - res.redirect((req.user) ? '/settings' : '/login') - } - }) - -router.route('/password/:token') - - // Check token - .all( async (req, res, next) => { - debug('/settings/password/:token .all() called') - try { - let user = await User - .findOne({'auth.passToken': req.params.token}) - .where('auth.passTokenExpires').gt(Date.now()) - - if (!user) { - debug('Bad token') - req.flash('danger', 'Password reset token is invalid or has expired. ') - res.redirect((req.isAuthenticated) ? '/settings' : '/login') - } else { - debug('setting passwordUser') - res.locals.passwordUser = user - next() - } - - } catch (err) { - mw.throwErr(err, req) - res.redirect('/password') - } - - }) - - // Show password change form - .get((req, res) => { - debug('/settings/password/:token .get() called') - res.render('password') - }) - - // Set new password - .post( async (req, res, next) => { - debug('/settings/password/:token .post() called') - - // Validate password strength - let zxcvbnResult = zxcvbn(req.body.password) - if (zxcvbnResult.crack_times_seconds.online_no_throttling_10_per_second < 864000) { // Less than ten days - req.flash( 'danger', - `That password could be cracked in ${zxcvbnResult.crack_times_display.online_no_throttling_10_per_second}! Come up with a more complex password that would take at least 10 days to crack. ` - ) - res.redirect(`/settings/password/${req.params.token}`) - } else { - - // Create hashed password and save to db - try { - await res.locals.passwordUser.generateHashedPassword(req.body.password) - - // User changed password - if (req.user) { - debug('User saved password') - req.flash('success', 'Your password has been changed. ') - res.redirect('/settings') - - // New user created password - } else { - debug('New user created password') - req.flash('success', 'Password set. You can use it to log in now. ') - res.redirect('/login') - } - - } catch (err) { - debug('Error creating hashed password and saving to db') - mw.throwErr(err, req) - res.redirect(`/settings/password/${req.params.token}`) - } - - } - }) - // Tracman pro router.route('/pro') .all(mw.ensureAuth, (req, res, next) => { @@ -322,7 +168,7 @@ router.route('/pro') // Join Tracman pro .post( async (req, res) => { try { - let user = await User.findByIdAndUpdate(req.user.id, + await User.findByIdAndUpdate(req.user.id, {$set: { isPro: true }}) req.flash('success', 'You have been signed up for pro. ') res.redirect('/settings') @@ -332,4 +178,18 @@ router.route('/pro') } }) +// Redirects for URLs that moved to /account +router.route('/password') + .all((req,res)=>{ + res.redirect(307, '/account/password') + }) +router.route('/password/:token') + .all((req,res)=>{ + res.redirect(307, `/account/password/${req.params.token}`) + }) +router.route('/email/:token') + .all((req,res)=>{ + res.redirect(307, `/account/email/${req.params.token}`) + }) + module.exports = router diff --git a/config/sockets.js b/config/sockets.js index 406287d..278ebf3 100755 --- a/config/sockets.js +++ b/config/sockets.js @@ -2,6 +2,7 @@ // Imports const debug = require('debug')('tracman-sockets') +const sanitize = require('mongo-sanitize') const User = require('./models.js').user // Check for tracking clients @@ -82,7 +83,7 @@ module.exports = { } else { try { // Get loc.usr - let user = await User.findById(loc.usr) + let user = await User.findById(sanitize(loc.usr)) .where('sk32').equals(loc.tok) if (!user) { @@ -95,7 +96,8 @@ module.exports = { } else { // Check that loc is newer than lastLoc - debug(`Checking that loc of ${loc.tim} is newer than last of ${user.last.time.getTime()}...`) + debug(`Checking that loc of ${loc.tim} is newer than last of + ${(user.last.time)?user.last.time.getTime():user.last.time}...`) if (!user.last.time || loc.tim > user.last.time.getTime()) { // Broadcast location diff --git a/config/test.js b/config/test.js index 377c86a..f654dd3 100755 --- a/config/test.js +++ b/config/test.js @@ -4,5 +4,5 @@ module.exports = { TEST_PASSWORD: 'mDAQYe2VYE', BAD_PASSWORD: 'password123', FUZZED_EMAIL_TRIES: 3, - FUZZED_PASSWORD_TRIES: 10, + FUZZED_PASSWORD_TRIES: 100, } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a963d02..097175b 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,60 +1,9 @@ { "name": "tracman", - "version": "0.7.12", + "version": "0.8.2", "lockfileVersion": 1, "requires": true, "dependencies": { - "@types/bluebird": { - "version": "3.5.19", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.19.tgz", - "integrity": "sha512-2nHw8pBp6J0N4mHPEO5GJptmd0KKjLFz/wpBiLMOT8UVnGqAP2e7P44wKVj+ujPvsFuIGyB2waDA3dpYX3c6Aw==" - }, - "@types/body-parser": { - "version": "1.16.8", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.16.8.tgz", - "integrity": "sha512-BdN2PXxOFnTXFcyONPW6t0fHjz2fvRZHVMFpaS0wYr+Y8fWEaNOs4V8LEu/fpzQlMx+ahdndgTaGTwPC+J/EeA==", - "requires": { - "@types/express": "4.0.39", - "@types/node": "8.5.2" - } - }, - "@types/express": { - "version": "4.0.39", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.0.39.tgz", - "integrity": "sha512-dBUam7jEjyuEofigUXCtublUHknRZvcRgITlGsTbFgPvnTwtQUt2NgLakbsf+PsGo/Nupqr3IXCYsOpBpofyrA==", - "requires": { - "@types/body-parser": "1.16.8", - "@types/express-serve-static-core": "4.11.0", - "@types/serve-static": "1.13.1" - } - }, - "@types/express-serve-static-core": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.11.0.tgz", - "integrity": "sha512-hOi1QNb+4G+UjDt6CEJ6MjXHy+XceY7AxIa28U9HgJ80C+3gIbj7h5dJNxOI7PU3DO1LIhGP5Bs47Dbf5l8+MA==", - "requires": { - "@types/node": "8.5.2" - } - }, - "@types/mime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", - "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" - }, - "@types/node": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.2.tgz", - "integrity": "sha512-KA4GKOpgXnrqEH2eCVhiv2CsxgXGQJgV1X0vsGlh+WCnxbeAE1GT44ZsTU1IN5dEeV/gDupKa7gWo08V5IxWVQ==" - }, - "@types/serve-static": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.1.tgz", - "integrity": "sha512-jDMH+3BQPtvqZVIcsH700Dfi8Q3MIcEx16g/VdxjoqiGR/NntekB10xdBpirMKnPe9z2C5cBmL0vte0YttOr3Q==", - "requires": { - "@types/express-serve-static-core": "4.11.0", - "@types/mime": "2.0.0" - } - }, "a-sync-waterfall": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.0.tgz", @@ -116,6 +65,15 @@ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, + "agent-base": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", + "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", + "dev": true, + "requires": { + "es6-promisify": "5.0.0" + } + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -147,6 +105,12 @@ "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, "ansi-align": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", @@ -214,6 +178,15 @@ "normalize-path": "2.1.1" } }, + "append-transform": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", + "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", + "dev": true, + "requires": { + "default-require-extensions": "1.0.0" + } + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -390,6 +363,97 @@ "js-tokens": "3.0.2" } }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.7", + "trim-right": "1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + } + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "2.5.3", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.3", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -469,11 +533,6 @@ "inherits": "2.0.3" } }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - }, "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", @@ -766,6 +825,11 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" }, + "camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, "caniuse-api": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", @@ -878,6 +942,12 @@ "supports-color": "2.0.0" } }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -891,6 +961,7 @@ "requires": { "anymatch": "1.3.2", "async-each": "1.0.1", + "fsevents": "1.1.3", "glob-parent": "2.0.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -937,6 +1008,25 @@ "restore-cursor": "1.0.1" } }, + "cli-table2": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/cli-table2/-/cli-table2-0.2.0.tgz", + "integrity": "sha1-LR738hig54biFFQFYtS9F3/jLZc=", + "dev": true, + "requires": { + "colors": "1.1.2", + "lodash": "3.10.1", + "string-width": "1.0.2" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + } + } + }, "cli-width": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", @@ -1114,6 +1204,11 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" }, + "content-security-policy-builder": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz", + "integrity": "sha512-j+Nhmj1yfZAikJLImCvPJFE29x/UuBi+/MWqggGGc515JKaZrjuei2RhULJmy0MsstW3E3htl002bwmBNMKr7w==" + }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", @@ -1174,11 +1269,38 @@ "keygrip": "1.0.2" } }, + "core-js": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", + "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "coveralls": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.0.tgz", + "integrity": "sha512-ZppXR9y5PraUOrf/DzHJY6gzNUhXYE3b9D43xEXs4QYZ7/Oe0Gy0CS+IPKWFfvQFXB3RG9QduaQUFehzSpGAFw==", + "dev": true, + "requires": { + "js-yaml": "3.7.0", + "lcov-parse": "0.0.10", + "log-driver": "1.2.7", + "minimist": "1.2.0", + "request": "2.83.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "create-ecdh": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", @@ -1273,6 +1395,16 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "csrf": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.6.tgz", + "integrity": "sha1-thEg3c7q/JHnbtUxO7XAsmZ7cQo=", + "requires": { + "rndm": "1.2.0", + "tsscmp": "1.0.5", + "uid-safe": "2.1.4" + } + }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -1372,6 +1504,40 @@ "source-map": "0.5.7" } }, + "csurf": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.9.0.tgz", + "integrity": "sha1-SdLGkl/87Ht95VlZfBU/pTM2QTM=", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "csrf": "3.0.6", + "http-errors": "1.5.1" + }, + "dependencies": { + "http-errors": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz", + "integrity": "sha1-eIwNLB3iyBuebowBhDtrl+uSB1A=", + "requires": { + "inherits": "2.0.3", + "setprototypeof": "1.0.2", + "statuses": "1.4.0" + } + }, + "setprototypeof": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz", + "integrity": "sha1-gaVSFB7BBLiOic44MQOtXGZWTQg=" + } + } + }, + "cvss": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cvss/-/cvss-1.0.2.tgz", + "integrity": "sha1-32fpK/EqeW9J6Sh5nI2zunS5/NY=", + "dev": true + }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", @@ -1388,6 +1554,11 @@ "assert-plus": "1.0.0" } }, + "dasherize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", + "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" + }, "date-now": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", @@ -1432,6 +1603,26 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "default-require-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", + "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", + "dev": true, + "requires": { + "strip-bom": "2.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, "define-properties": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", @@ -1505,6 +1696,15 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, "diff": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", @@ -1521,6 +1721,11 @@ "randombytes": "2.0.5" } }, + "dns-prefetch-control": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz", + "integrity": "sha1-YN20V3dOF48flBXwyrsOhbCzALI=" + }, "doctrine": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.2.tgz", @@ -1535,6 +1740,11 @@ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=" }, + "dont-sniff-mimetype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz", + "integrity": "sha1-WTKJDcn04vGeXrAqIAJuXl78j1g=" + }, "dot-prop": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", @@ -1759,6 +1969,23 @@ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "4.2.4" + }, + "dependencies": { + "es6-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", + "dev": true + } + } + }, "es6-set": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", @@ -2098,6 +2325,11 @@ "fill-range": "2.2.3" } }, + "expect-ct": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.1.0.tgz", + "integrity": "sha1-UnNWeN4YUwiQ2Ne5XwrGNkCVgJQ=" + }, "express": { "version": "4.16.2", "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", @@ -2147,23 +2379,27 @@ } } }, - "express-validator": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-3.2.1.tgz", - "integrity": "sha1-RWA+fu5pMYXCGY+969QUkl/9NSQ=", - "requires": { - "@types/bluebird": "3.5.19", - "@types/express": "4.0.39", - "bluebird": "3.5.1", - "lodash": "4.17.4", - "validator": "6.2.1" - } + "express-request-limit": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/express-request-limit/-/express-request-limit-1.0.2.tgz", + "integrity": "sha1-gVjPr8A5VFEAjH3Hm/2zYTaDSB4=" }, "extend": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" }, + "external-editor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", + "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==", + "dev": true, + "requires": { + "chardet": "0.4.2", + "iconv-lite": "0.4.19", + "tmp": "0.0.33" + } + }, "extglob": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", @@ -2223,6 +2459,16 @@ "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "requires": { + "glob": "7.1.2", + "minimatch": "3.0.4" + } + }, "fill-range": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", @@ -2337,6 +2583,11 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" }, + "frameguard": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.0.0.tgz", + "integrity": "sha1-e8rUae57lukdEs6zlZx4I1qScuk=" + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -2353,6 +2604,795 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "fsevents": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", + "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", + "optional": true, + "requires": { + "nan": "2.6.2", + "node-pre-gyp": "0.6.39" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.39", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "1.0.2", + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + } + } + }, "fstream": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", @@ -2528,6 +3568,35 @@ "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", "dev": true }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -2622,6 +3691,42 @@ "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", "dev": true }, + "helmet": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.12.0.tgz", + "integrity": "sha512-CgkctpvreQLL6X3EL2Igs/92+75ZFIsrob9/Rdwf2hQCBGH/DxLk4xFPxAAl6jYnnus/YXfFEVXHEJf8TJTwlA==", + "requires": { + "dns-prefetch-control": "0.1.0", + "dont-sniff-mimetype": "1.0.0", + "expect-ct": "0.1.0", + "frameguard": "3.0.0", + "helmet-csp": "2.7.0", + "hide-powered-by": "1.0.0", + "hpkp": "2.0.0", + "hsts": "2.1.0", + "ienoopen": "1.0.0", + "nocache": "2.0.0", + "referrer-policy": "1.1.0", + "x-xss-protection": "1.1.0" + } + }, + "helmet-csp": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.7.0.tgz", + "integrity": "sha512-IGIAkWnxjRbgMXFA2/kmDqSIrIaSfZ6vhMHlSHw7jm7Gm9nVVXqwJ2B1YEpYrJsLrqY+w2Bbimk7snux9+sZAw==", + "requires": { + "camelize": "1.0.0", + "content-security-policy-builder": "2.0.0", + "dasherize": "2.0.0", + "lodash.reduce": "4.6.0", + "platform": "1.3.5" + } + }, + "hide-powered-by": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.0.0.tgz", + "integrity": "sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=" + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -2671,6 +3776,16 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" }, + "hpkp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", + "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" + }, + "hsts": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.1.0.tgz", + "integrity": "sha512-zXhh/DqgrTXJ7erTN6Fh5k/xjMhDGXCqdYN3wvxUvGUQvnxcFfUd8E+6vLg/nk3ss1TYMb+DhRl25fYABioTvA==" + }, "html-comment-regex": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz", @@ -2702,6 +3817,27 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "https-proxy-agent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.0.tgz", + "integrity": "sha512-uUWcfXHvy/dwfM9bqa6AozvAjS32dZSTUYd/4SEpYKRg6LEcPLshksnQYRudM9AyNvUARMfAg5TLjUDyX/K4vA==", + "dev": true, + "requires": { + "agent-base": "4.2.0", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, "iconv-lite": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", @@ -2773,6 +3909,11 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" }, + "ienoopen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.0.0.tgz", + "integrity": "sha1-NGpCj0dKrI9QzzeE6i0PFvYr2ms=" + }, "ignore": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", @@ -2852,6 +3993,15 @@ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" }, + "invariant": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.3.tgz", + "integrity": "sha512-7Z5PPegwDTyjbaeCnV0efcyS6vdKAU51kpEmS7QFib3P4822l8ICYyMn7qvJnc+WzLoDsuI9gPMKbJ8pCu8XtA==", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", @@ -2934,6 +4084,15 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -3040,6 +4199,12 @@ "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, "is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -3097,6 +4262,12 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -3120,6 +4291,156 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "istanbul": { + "version": "1.0.0-alpha.2", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-1.0.0-alpha.2.tgz", + "integrity": "sha1-BglrwI6Yuq10Sq5Gli2N+frGPQg=", + "dev": true, + "requires": { + "abbrev": "1.0.9", + "async": "1.5.2", + "istanbul-api": "1.2.2", + "js-yaml": "3.7.0", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "which": "1.3.0", + "wordwrap": "1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.0.9" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "istanbul-api": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.2.2.tgz", + "integrity": "sha512-kH5YRdqdbs5hiH4/Rr1Q0cSAGgjh3jTtg8vu9NLebBAoK3adVO4jk81J+TYOkTr2+Q4NLeb1ACvmEt65iG/Vbw==", + "dev": true, + "requires": { + "async": "2.1.4", + "fileset": "2.0.3", + "istanbul-lib-coverage": "1.1.2", + "istanbul-lib-hook": "1.1.0", + "istanbul-lib-instrument": "1.9.2", + "istanbul-lib-report": "1.1.3", + "istanbul-lib-source-maps": "1.2.3", + "istanbul-reports": "1.1.4", + "js-yaml": "3.7.0", + "mkdirp": "0.5.1", + "once": "1.4.0" + } + }, + "istanbul-lib-coverage": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.2.tgz", + "integrity": "sha512-tZYA0v5A7qBSsOzcebJJ/z3lk3oSzH62puG78DbBA1+zupipX2CakDyiPV3pOb8He+jBwVimuwB0dTnh38hX0w==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz", + "integrity": "sha512-U3qEgwVDUerZ0bt8cfl3dSP3S6opBoOtk3ROO5f2EfBr/SRiD9FQqzwaZBqFORu8W7O0EXpai+k7kxHK13beRg==", + "dev": true, + "requires": { + "append-transform": "0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.2.tgz", + "integrity": "sha512-nz8t4HQ2206a/3AXi+NHFWEa844DMpPsgbcUteJbt1j8LX1xg56H9rOMnhvcvVvPbW60qAIyrSk44H8ZDqaSSA==", + "dev": true, + "requires": { + "babel-generator": "6.26.1", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "istanbul-lib-coverage": "1.1.2", + "semver": "5.4.1" + } + }, + "istanbul-lib-report": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.3.tgz", + "integrity": "sha512-D4jVbMDtT2dPmloPJS/rmeP626N5Pr3Rp+SovrPn1+zPChGHcggd/0sL29jnbm4oK9W0wHjCRsdch9oLd7cm6g==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "1.1.2", + "mkdirp": "0.5.1", + "path-parse": "1.0.5", + "supports-color": "3.2.3" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.3.tgz", + "integrity": "sha512-fDa0hwU/5sDXwAklXgAoCJCOsFsBplVQ6WBldz5UwaqOzmDhUK4nfuR7/G//G2lERlblUNJB8P6e8cXq3a7MlA==", + "dev": true, + "requires": { + "debug": "3.1.0", + "istanbul-lib-coverage": "1.1.2", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "source-map": "0.5.7" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "istanbul-reports": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.1.4.tgz", + "integrity": "sha512-DfSTVOTkuO+kRmbO8Gk650Wqm1WRGr6lrdi2EwDK1vxpS71vdlLd613EpzOKdIFioB5f/scJTjeWBnvd1FWejg==", + "dev": true, + "requires": { + "handlebars": "4.0.11" + } + }, "jquery": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz", @@ -3294,6 +4615,12 @@ "invert-kv": "1.0.0" } }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", + "dev": true + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -3375,16 +4702,36 @@ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, "lowercase-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", @@ -3583,9 +4930,9 @@ } }, "mocha": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.0.1.tgz", - "integrity": "sha512-evDmhkoA+cBNiQQQdSKZa2b9+W2mpLoj50367lhy+Klnx9OV8XlCIhigUnn1gaTFLQCa0kdNhEGDr0hCXOQFDw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", + "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", "dev": true, "requires": { "browser-stdout": "1.3.0", @@ -3632,11 +4979,22 @@ } } }, + "mocha-froth": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/mocha-froth/-/mocha-froth-0.2.1.tgz", + "integrity": "sha512-EHeNGZIA9+L0E9o8o7QAeA70KeakhgQO49zXDj+1AZcrHsnme3RAWb8cBrdpnyxenmfXsCDSwVGFl12c2MO5nA==", + "dev": true + }, "moment": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" }, + "mongo-sanitize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mongo-sanitize/-/mongo-sanitize-1.0.0.tgz", + "integrity": "sha1-FeMRMEivvz50RkxOgVaCG4/6wdw=" + }, "mongodb": { "version": "2.2.33", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.33.tgz", @@ -3766,6 +5124,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "nocache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz", + "integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=" + }, "node-libs-browser": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", @@ -3833,6 +5196,12 @@ "update-notifier": "2.3.0" } }, + "nodesecurity-npm-utils": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nodesecurity-npm-utils/-/nodesecurity-npm-utils-6.0.0.tgz", + "integrity": "sha512-NLRle1woNaT2orR6fue2jNqkhxDTktgJj3sZxvR/8kp21pvOY7Gwlx5wvo0H8ZVPqdgd2nE2ADB9wDu5Cl8zNg==", + "dev": true + }, "nopt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", @@ -3896,6 +5265,263 @@ "set-blocking": "2.0.0" } }, + "nsp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/nsp/-/nsp-3.2.1.tgz", + "integrity": "sha512-dLmGi7IGixJEHKetErIH460MYiYIzAoxuVsloZFu9e1p9U8K0yULx7YQ1+VzrjZbB+wqq67ES1SfOvKVb/qMDQ==", + "dev": true, + "requires": { + "chalk": "2.3.2", + "cli-table2": "0.2.0", + "cvss": "1.0.2", + "https-proxy-agent": "2.2.0", + "inquirer": "3.3.0", + "nodesecurity-npm-utils": "6.0.0", + "semver": "5.4.1", + "wreck": "12.5.1", + "yargs": "9.0.1" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz", + "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.3.0" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + } + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "requires": { + "ansi-escapes": "3.0.0", + "chalk": "2.3.2", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "2.1.0", + "figures": "2.0.0", + "lodash": "4.17.4", + "mute-stream": "0.0.7", + "run-async": "2.3.0", + "rx-lite": "4.0.8", + "rx-lite-aggregates": "4.0.8", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "1.1.0" + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "dev": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "2.1.0" + } + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + }, + "supports-color": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "yargs": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-9.0.1.tgz", + "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=", + "dev": true, + "requires": { + "camelcase": "4.1.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "read-pkg-up": "2.0.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "7.0.0" + } + } + } + }, "num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", @@ -4026,6 +5652,16 @@ "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "dev": true }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.2" + } + }, "optionator": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", @@ -4587,6 +6223,11 @@ } } }, + "platform": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", + "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==" + }, "pluralize": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", @@ -5212,6 +6853,11 @@ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", @@ -5396,11 +7042,22 @@ } } }, + "referrer-policy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.1.0.tgz", + "integrity": "sha1-NXdOtzW/UPtsB46DM0tHI1AgfXk=" + }, "regenerate": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==" }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, "regex-cache": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", @@ -5471,6 +7128,15 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, "request": { "version": "2.83.0", "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", @@ -5586,6 +7252,11 @@ "inherits": "2.0.3" } }, + "rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=" + }, "run-async": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", @@ -5607,6 +7278,15 @@ "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", "dev": true }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "3.1.2" + } + }, "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", @@ -6246,6 +7926,15 @@ "setimmediate": "1.0.5" } }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + }, "to-array": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", @@ -6256,6 +7945,12 @@ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -6284,6 +7979,17 @@ "punycode": "1.4.1" } }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tsscmp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz", + "integrity": "sha1-fcSjOvcVgatDN9qR2FylQn69mpc=" + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -6364,6 +8070,14 @@ "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" }, + "uid-safe": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.4.tgz", + "integrity": "sha1-Otbzg2jG1MjHXsF2I/t5qh0HHYE=", + "requires": { + "random-bytes": "1.0.0" + } + }, "uid2": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", @@ -6556,11 +8270,6 @@ "spdx-expression-parse": "1.0.4" } }, - "validator": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/validator/-/validator-6.2.1.tgz", - "integrity": "sha1-vFdbeNFb6y4zimZbqVMMf0Ce9mc=" - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6837,6 +8546,27 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "wreck": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/wreck/-/wreck-12.5.1.tgz", + "integrity": "sha512-l5DUGrc+yDyIflpty1x9XuMj1ehVjC/dTbF3/BasOO77xk0EdEa4M/DuOY8W88MQDAD0fEDqyjc8bkIMHd2E9A==", + "dev": true, + "requires": { + "boom": "5.2.0", + "hoek": "4.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "dev": true, + "requires": { + "hoek": "4.2.0" + } + } + } + }, "write": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", @@ -6867,6 +8597,11 @@ "ultron": "1.1.1" } }, + "x-xss-protection": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.1.0.tgz", + "integrity": "sha512-rx3GzJlgEeZ08MIcDsU2vY2B1QEriUKJTSiNHHUIem6eg9pzVOr2TL3Y4Pd6TMAM5D5azGjcxqI62piITBDHVg==" + }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", diff --git a/package.json b/package.json index ffa709f..b36a745 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tracman", - "version": "0.8.2", + "version": "0.9.0", "description": "Tracks user's GPS location", "main": "server.js", "dependencies": { @@ -10,13 +10,17 @@ "cookie-parser": "^1.4.3", "cookie-session": "^2.0.0-beta.2", "css-loader": "^0.28.7", + "csurf": "^1.9.0", "debug": "^2.6.9", "express": "^4.15.5", - "express-validator": "^3.2.1", + "express-request-limit": "^1.0.2", + "helmet": "^3.12.0", + "helmet-csp": "^2.7.0", "jquery": "^3.2.1", "load-google-maps-api": "^1.0.0", "minifier": "^0.8.1", "moment": "^2.18.1", + "mongo-sanitize": "^1.0.0", "mongoose": "^4.11.13", "mongoose-unique-validator": "^1.0.6", "nodemailer": "^4.1.1", @@ -42,21 +46,27 @@ "devDependencies": { "chai": "^4.1.2", "chai-http": "^3.0.0", + "coveralls": "^3.0.0", + "istanbul": "^1.0.0-alpha.2", "mocha": "^4.0.1", + "mocha-froth": "^0.2.1", "nodemon": "^1.11.0", + "nsp": "^3.2.1", "standard": "^10.0.3", "superagent": "^3.8.2", "supertest": "^3.0.0" }, "scripts": { - "test": "mocha", + "test": "node_modules/mocha/bin/_mocha --exit", + "cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha -- --exit test/*", + "audit": "node_modules/nsp/bin/nsp audit-package ; node_modules/nsp/bin/nsp audit-shrinkwrap", "lint": "standard", "start": "node server.js", "nodemon": "nodemon --ignore 'static/**/*.min.*' server.js", "update": "sudo npm update && sudo npm prune", "minify": "minify --template .{{filename}}.min.{{ext}} --clean static/css*", - "build": "./node_modules/.bin/webpack --config webpack.config.js", - "subuild": "sudo ./node_modules/.bin/webpack --config webpack.config.js" + "build": "node_modules/.bin/webpack --config webpack.config.js", + "subuild": "sudo node_modules/.bin/webpack --config webpack.config.js" }, "repository": "Tracman-org/Server", "keywords": [ @@ -69,5 +79,5 @@ "license": "GPL-3.0", "README": "README.md", "bugs": "https://github.com/Tracman-org/Server/issues", - "homepage": "https://tracman.org/" + "homepage": "https://www.tracman.org/" } diff --git a/server.js b/server.js index 6ca111c..a6381ca 100755 --- a/server.js +++ b/server.js @@ -2,10 +2,13 @@ /* IMPORTS */ const express = require('express') +const helmet = require('helmet') +const csp = require('helmet-csp') +const rateLimit = require('express-request-limit') const bodyParser = require('body-parser') -const expressValidator = require('express-validator') const cookieParser = require('cookie-parser') const cookieSession = require('cookie-session') +const csurf = require('csurf') const mongoose = require('mongoose') const nunjucks = require('nunjucks') const passport = require('passport') @@ -48,31 +51,77 @@ let ready_promise_list = [] /* Templates */ { nunjucks.configure(__dirname + '/views', { autoescape: true, - express: app + express: app, }) app.set('view engine', 'html') } -/* Session */ { - app.use(cookieParser(env.cookie)) - app.use(cookieSession({ - cookie: {maxAge: 60000}, +/* Express session and settings */ app.use( + helmet.referrerPolicy({ + policy: 'strict-origin', + }), + csp({directives:{ + 'default-src': ["'self'"], + 'script-src': ["'self'", + "'unsafe-inline'", // TODO: Get rid of this + 'https://code.jquery.com', + 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/*', + 'https://www.google.com/recaptcha', + 'https://www.google-analytics.com', + 'https://maps.googleapis.com', + 'https://coin-hive.com', + 'https://coinhive.com', + ], + 'worker-src': ["'self'", + 'blob:', // for coinhive + ], + 'connect-src': ["'self'", + 'wss://*.tracman.org', + 'wss://*.coinhive.com', + ], + 'style-src': ["'self'", + "'unsafe-inline'", + 'https://fonts.googleapis.com', + 'https://maxcdn.bootstrapcdn.com', + ], + 'font-src': ['https://fonts.gstatic.com'], + 'img-src': ["'self'", + 'https://www.google-analytics.com', + 'https://maps.gstatic.com', + 'https://maps.googleapis.com', + 'https://http.cat', + ], + 'object-src': ["'none'"], + 'report-uri': '/csp-violation', + }}), + cookieParser(env.cookie), + cookieSession({ + cookie: { + maxAge: 1000 * 60 * 60 * 24 * 7, // 1 week + secure: true, + httpOnly: true, + domain: env.url.substring(env.url.indexOf('//')+2), + }, secret: env.session, saveUninitialized: true, - resave: true - })) - app.use(bodyParser.json()) - app.use(bodyParser.urlencoded({ - extended: true - })) - app.use(expressValidator()) - app.use(flash()) -} + resave: true, + }), + bodyParser.json(), + bodyParser.urlencoded({ + extended: true, + }), + flash() +) + +/* Report CSP violations */ +app.post('/csp-violation', (req, res) => { + console.log(`CSP Violation: ${JSON.stringify(req.body)}`) + res.status(204).end() +}) /* Auth */ { require('./config/passport.js')(passport) - app.use(passport.initialize()) - app.use(passport.session()) + app.use(passport.initialize(), passport.session()) } /* Routes */ { @@ -82,6 +131,13 @@ let ready_promise_list = [] // Default locals available to all views (keep this after static files) app.get('*', (req, res, next) => { + // Rate limit + rateLimit({ + timeout: 1000 * 60 * 30, // 30 minutes + exactPath: true, + cleanUpInterval: 1000 * 60 * 60 * 24 * 7, // 1 week + }) + // User account res.locals.user = req.user @@ -105,6 +161,9 @@ let ready_promise_list = [] // Settings app.use('/settings', require('./config/routes/settings.js')) + // Account settings + app.use('/account', require('./config/routes/account.js')) + // Map app.use(['/map', '/trac'], require('./config/routes/map.js')) @@ -133,7 +192,7 @@ let ready_promise_list = [] res.status(err.status || 500) res.render('error', { code: err.status || 500, - message: (err.status <= 499) ? err.message : 'Server error' + message: (err.status < 500) ? err.message : 'Server error' }) }) @@ -152,6 +211,11 @@ let ready_promise_list = [] } } +// CSRF Protection (keep after routes) +app.use(csurf({ + cookie: true, + })) + /* Sockets */ { sockets.init(io) } diff --git a/static/js/map.js b/static/js/map.js index fb33668..a84f1c7 100755 --- a/static/js/map.js +++ b/static/js/map.js @@ -237,8 +237,8 @@ loadGoogleMapsAPI({ key: mapKey }) if (noHeader !== '0' && mapuser._id !== 'demo') { const logoDiv = document.createElement('div') logoDiv.id = 'map-logo' - logoDiv.innerHTML = '' + - '[]' + + logoDiv.innerHTML = '' + + '[]' + "Tracman" map.controls[googlemaps.ControlPosition.BOTTOM_LEFT].push(logoDiv) } diff --git a/static/js/settings.js b/static/js/settings.js index 212c256..4a51421 100755 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -21,7 +21,7 @@ $(function () { var slugNotUnique, emailNotUnique // Set timezone in password change link - $('#password').attr('href', '/settings/password?tz=' + new Date().getTimezoneOffset()) + $('#password').attr('href', '/account/password?tz=' + new Date().getTimezoneOffset()) // Delete account $('#delete').click(function () { diff --git a/static/sw.js b/static/js/sw.js similarity index 100% rename from static/sw.js rename to static/js/sw.js diff --git a/test/auth.js b/test/auth.js index 125007d..f3233c2 100755 --- a/test/auth.js +++ b/test/auth.js @@ -2,6 +2,7 @@ const chai = require('chai') const app = require('../server') +const froth = require('mocha-froth') const User = require('../config/models').user // const superagent = require('superagent').agent() const request = require('supertest').agent(app) @@ -37,10 +38,10 @@ describe('Authentication', () => { ).to.redirectTo('/login#signup') /* Ensure user was deleted after email failed to send - /* Users with bad emails are removed asynchronously and may happen after - /* the response was recieved. Ensure it's happened in a kludgy way by - /* waiting 2 seconds before asserting that the user doesn't exist - */ + * Users with bad emails are removed asynchronously and may happen after + * the response was recieved. Ensure it's happened in a kludgy way by + * waiting 2 seconds before asserting that the user doesn't exist + */ setTimeout( async () => { chai.assert.isNull( await User.findOne({ 'email': FAKE_EMAIL @@ -49,27 +50,28 @@ describe('Authentication', () => { }) - // TODO: Implement fuzzer - it.skip(`Fails to create accounts with ${FUZZED_EMAIL_TRIES} fuzzed emails`, () => { + it(`Fails to create accounts with ${FUZZED_EMAIL_TRIES} fuzzed emails`, () => { // Fuzz emails - // loop with let fuzzed_email + froth(FUZZED_EMAIL_TRIES).forEach( async (fuzzed_email) => { // Confirm redirect - // chai.expect( await request.post('/signup') - // .type('form').send({ 'email':fuzzed_email }) - // ).to.redirectTo('/login#signup') + chai.expect( await request.post('/signup') + .type('form').send({ 'email':fuzzed_email }) + ).to.redirectTo('/login#signup') - /* Ensure user was deleted after email failed to send - /* Users with bad emails are removed asynchronously and may happen after - /* the response was recieved. Ensure it's happened in a kludgy way by - /* waiting 2 seconds before asserting that the user doesn't exist - */ - // setTimeout( async () => { - // chai.assert.isNull( await User.findOne({ - // 'email': FAKE_EMAIL - // }), 'Account with fake email was created') - // }, 2000) + /* Ensure user was deleted after email failed to send + * Users with bad emails are removed asynchronously and may happen after + * the response was recieved. Ensure it's happened in a kludgy way by + * waiting 2 seconds before asserting that the user doesn't exist + */ + setTimeout( async () => { + chai.assert.isNull( await User.findOne({ + 'email': fuzzed_email + }), 'Account with fake email was created') + }, 2000) + + }) }) @@ -89,97 +91,86 @@ describe('Authentication', () => { it('Loads password page', async () => { // Load password page chai.expect(await request - .get(`/settings/password/${passwordless_user.auth.passToken}`) - ).html.to.have.status(200) + .get(`/account/password/${passwordless_user.auth.passToken}`) + ).to.be.html.and.have.status(200) }) it('Fails to set a weak password', async () => { chai.expect( await request - .post(`/settings/password/${passwordless_user.auth.passToken}`) + .post(`/account/password/${passwordless_user.auth.passToken}`) .type('form').send({ 'password':BAD_PASSWORD }) - ).to.redirectTo(`/settings/password/${passwordless_user.auth.passToken}`) + ).to.redirectTo(`/account/password/${passwordless_user.auth.passToken}`) }) it('Sets a strong password', async () => { - try { - // Perform request - let res = await request - .post(`/settings/password/${passwordless_user.auth.passToken}`) - .type('form').send({ 'password':TEST_PASSWORD }) + // Perform request + let res = await request + .post(`/account/password/${passwordless_user.auth.passToken}`) + .type('form').send({ 'password':TEST_PASSWORD }) - // Expect redirect - chai.expect(res).to.redirectTo('/login') + // Expect redirect + chai.expect(res).to.redirectTo('/login') - // Retrieve user with password saved - let passworded_user = await User.findOne({'email':TEST_EMAIL} ) + // Retrieve user with password saved + let passworded_user = await User.findOne({'email':TEST_EMAIL} ) - // Assert password was set - chai.assert.isString( - passworded_user.auth.password, 'Failed to correctly save password' - ) + // Assert password was set + chai.assert.isString( + passworded_user.auth.password, 'Failed to correctly save password' + ) + + return res - return res - } catch (err) { throw err } }) // These tests require the test user to have been created after( () => { - describe('Logged out', () => { + describe('Logged out', function() { - it('Fails to log in with bad password', async () => { + // Password fuzzing could take a while... give it five seconds + this.timeout(5000) - // Confirm redirect - chai.expect( await request.post('/login') - .type('form').send({ - 'email': TEST_EMAIL, - 'password': BAD_PASSWORD - }) - ).to.redirectTo('/login') // Hey! Incorrect email or password. - - }) - - // TODO: Implement fuzzer - it.skip(`Fails to log in with ${FUZZED_PASSWORD_TRIES} fuzzed passwords`, () => { + it(`Fails to log in with ${FUZZED_PASSWORD_TRIES} fuzzed passwords`, () => { // Fuzz passwords - // loop with let fuzzed_password + froth(FUZZED_PASSWORD_TRIES).forEach( async (fuzzed_password) => { - // Confirm redirect - // chai.expect( await request.post('/login') - // .type('form').send({ - // 'email': TEST_EMAIL, - // 'password': fuzzed_password - // }) - // ).to.redirectTo('/login') // Hey! Incorrect email or password. + // Confirm redirect + chai.expect( await request.post('/login') + .type('form').send({ + 'email': TEST_EMAIL, + 'password': fuzzed_password + }) + ).to.redirectTo('/login') // Hey! Incorrect email or password. + + }) }) it('Loads forgot password page', async () => { let res = await request.get('/login/forgot') - chai.expect(res).html.to.have.status(200) + chai.expect(res).to.be.html.and.have.status(200) }) // TODO: Test already-logged-in forgot password requests // TODO: Test invalid and fuzzed forgot password requests - // TODO: Fix this test - it.skip('Sends valid forgot password request', async () => { + it('Sends valid forgot password request', async () => { // Responds with 200 - let res = await request.post('/login/forgot') + chai.expect( await request.post('/login/forgot') .type('form').send({ - email: TEST_EMAIL, + 'email': TEST_EMAIL, }) - chai.expect(res).html.to.have.status(200) + ).to.redirectTo('/login') - // Assert password was set + // Assert password token was set let requesting_user = await User.findOne({'email':TEST_EMAIL} ) - chai.assert.isString( - requesting_user.auth.passwordToken, 'Failed to correctly save password token' - ) + chai.expect(requesting_user.auth.passToken) + .to.be.a('string').and.to.have.lengthOf(32) }) @@ -257,6 +248,7 @@ describe('Authentication', () => { }) }) + }) }) diff --git a/views/admin.html b/views/admin.html index 998351a..ec9b549 100755 --- a/views/admin.html +++ b/views/admin.html @@ -53,6 +53,7 @@ + - + - + {% endblock %} diff --git a/views/password.html b/views/password.html index 3d99329..d0b8cd2 100755 --- a/views/password.html +++ b/views/password.html @@ -18,22 +18,23 @@

Set Password

- + + -

Your password must be at least 8 characters long. You can use any letter, number, symbol, emoji, or spaces. Your password will be checked using zxcvbn. All passwords are stored stored on the server as salted hashes.

+

Your password must be at least 8 characters long. You can use any letter, number, symbol, emoji, or spaces. Your password will be checked using zxcvbn. All passwords are stored on the server as salted hashes.

diff --git a/views/pro.html b/views/pro.html index 4202bcb..cd60fa8 100755 --- a/views/pro.html +++ b/views/pro.html @@ -25,9 +25,10 @@

That said, just click the button below to try pro out.

Cheers,
- Keith Irwin

+ Keith Irwin

+ {% if user.isPro %}
diff --git a/views/settings.html b/views/settings.html index 6107e60..b10c6cf 100755 --- a/views/settings.html +++ b/views/settings.html @@ -19,6 +19,7 @@

Settings

+

Account settings

@@ -50,7 +51,7 @@
diff --git a/views/templates/base.html b/views/templates/base.html index 57634aa..ad2271e 100755 --- a/views/templates/base.html +++ b/views/templates/base.html @@ -3,7 +3,7 @@ +
diff --git a/webpack.config.js b/webpack.config.js index 469bb7d..42072ac 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,10 +12,9 @@ module.exports = { contact: './static/js/contact.js', login: './static/js/login.js', map: './static/js/map.js', - // controls: './static/js/controls.js', settings: './static/js/settings.js', password: './static/js/password.js', - sw: './static/sw.js', + sw: './static/js/sw.js', }, // Sourcemaps