├── .gitignore ├── auth ├── index.js ├── router.js └── strategies.js ├── config.js ├── package-lock.json ├── package.json ├── server.js ├── test ├── test-auth.js ├── test-protected.js └── test-users.js └── users ├── index.js ├── models.js └── router.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /auth/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {router} = require('./router'); 3 | const {localStrategy, jwtStrategy} = require('./strategies'); 4 | 5 | module.exports = {router, localStrategy, jwtStrategy}; 6 | -------------------------------------------------------------------------------- /auth/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const express = require('express'); 3 | const passport = require('passport'); 4 | const bodyParser = require('body-parser'); 5 | const jwt = require('jsonwebtoken'); 6 | 7 | const config = require('../config'); 8 | const router = express.Router(); 9 | 10 | const createAuthToken = function(user) { 11 | return jwt.sign({user}, config.JWT_SECRET, { 12 | subject: user.username, 13 | expiresIn: config.JWT_EXPIRY, 14 | algorithm: 'HS256' 15 | }); 16 | }; 17 | 18 | const localAuth = passport.authenticate('local', {session: false}); 19 | router.use(bodyParser.json()); 20 | // The user provides a username and password to login 21 | router.post('/login', localAuth, (req, res) => { 22 | const authToken = createAuthToken(req.user.serialize()); 23 | res.json({authToken}); 24 | }); 25 | 26 | const jwtAuth = passport.authenticate('jwt', {session: false}); 27 | 28 | // The user exchanges a valid JWT for a new one with a later expiration 29 | router.post('/refresh', jwtAuth, (req, res) => { 30 | const authToken = createAuthToken(req.user); 31 | res.json({authToken}); 32 | }); 33 | 34 | module.exports = {router}; 35 | -------------------------------------------------------------------------------- /auth/strategies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Strategy: LocalStrategy } = require('passport-local'); 3 | 4 | // Assigns the Strategy export to the name JwtStrategy using object destructuring 5 | // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Assigning_to_new_variable_names 6 | const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt'); 7 | 8 | const { User } = require('../users/models'); 9 | const { JWT_SECRET } = require('../config'); 10 | 11 | const localStrategy = new LocalStrategy((username, password, callback) => { 12 | let user; 13 | User.findOne({ username: username }) 14 | .then(_user => { 15 | user = _user; 16 | if (!user) { 17 | // Return a rejected promise so we break out of the chain of .thens. 18 | // Any errors like this will be handled in the catch block. 19 | return Promise.reject({ 20 | reason: 'LoginError', 21 | message: 'Incorrect username or password' 22 | }); 23 | } 24 | return user.validatePassword(password); 25 | }) 26 | .then(isValid => { 27 | if (!isValid) { 28 | return Promise.reject({ 29 | reason: 'LoginError', 30 | message: 'Incorrect username or password' 31 | }); 32 | } 33 | return callback(null, user); 34 | }) 35 | .catch(err => { 36 | if (err.reason === 'LoginError') { 37 | return callback(null, false, err); 38 | } 39 | return callback(err, false); 40 | }); 41 | }); 42 | 43 | const jwtStrategy = new JwtStrategy( 44 | { 45 | secretOrKey: JWT_SECRET, 46 | // Look for the JWT as a Bearer auth header 47 | jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'), 48 | // Only allow HS256 tokens - the same as the ones we issue 49 | algorithms: ['HS256'] 50 | }, 51 | (payload, done) => { 52 | done(null, payload.user); 53 | } 54 | ); 55 | 56 | module.exports = { localStrategy, jwtStrategy }; 57 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | exports.DATABASE_URL = process.env.DATABASE_URL || 'mongodb://localhost/jwt-auth-demo'; 3 | exports.TEST_DATABASE_URL = process.env.TEST_DATABASE_URL || 'mongodb://localhost/jwt-auth-demo'; 4 | exports.PORT = process.env.PORT || 8080; 5 | exports.JWT_SECRET = process.env.JWT_SECRET; 6 | exports.JWT_EXPIRY = process.env.JWT_EXPIRY || '7d'; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-jwt-auth", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.4", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", 10 | "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", 11 | "requires": { 12 | "mime-types": "2.1.17", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "array-flatten": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 20 | }, 21 | "assertion-error": { 22 | "version": "1.0.2", 23 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", 24 | "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", 25 | "dev": true 26 | }, 27 | "async": { 28 | "version": "2.1.4", 29 | "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", 30 | "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=", 31 | "requires": { 32 | "lodash": "4.17.4" 33 | } 34 | }, 35 | "balanced-match": { 36 | "version": "1.0.0", 37 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 38 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 39 | "dev": true 40 | }, 41 | "base64url": { 42 | "version": "2.0.0", 43 | "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", 44 | "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" 45 | }, 46 | "basic-auth": { 47 | "version": "2.0.0", 48 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", 49 | "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", 50 | "requires": { 51 | "safe-buffer": "5.1.1" 52 | } 53 | }, 54 | "bcryptjs": { 55 | "version": "2.4.3", 56 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", 57 | "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" 58 | }, 59 | "body-parser": { 60 | "version": "1.18.2", 61 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 62 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 63 | "requires": { 64 | "bytes": "3.0.0", 65 | "content-type": "1.0.4", 66 | "debug": "2.6.9", 67 | "depd": "1.1.1", 68 | "http-errors": "1.6.2", 69 | "iconv-lite": "0.4.19", 70 | "on-finished": "2.3.0", 71 | "qs": "6.5.1", 72 | "raw-body": "2.3.2", 73 | "type-is": "1.6.15" 74 | } 75 | }, 76 | "brace-expansion": { 77 | "version": "1.1.8", 78 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 79 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 80 | "dev": true, 81 | "requires": { 82 | "balanced-match": "1.0.0", 83 | "concat-map": "0.0.1" 84 | } 85 | }, 86 | "browser-stdout": { 87 | "version": "1.3.0", 88 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", 89 | "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", 90 | "dev": true 91 | }, 92 | "bson": { 93 | "version": "1.0.4", 94 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", 95 | "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" 96 | }, 97 | "buffer-equal-constant-time": { 98 | "version": "1.0.1", 99 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 100 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 101 | }, 102 | "buffer-shims": { 103 | "version": "1.0.0", 104 | "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", 105 | "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", 106 | "dev": true 107 | }, 108 | "bytes": { 109 | "version": "3.0.0", 110 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 111 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 112 | }, 113 | "chai": { 114 | "version": "4.1.2", 115 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", 116 | "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", 117 | "dev": true, 118 | "requires": { 119 | "assertion-error": "1.0.2", 120 | "check-error": "1.0.2", 121 | "deep-eql": "3.0.1", 122 | "get-func-name": "2.0.0", 123 | "pathval": "1.1.0", 124 | "type-detect": "4.0.5" 125 | } 126 | }, 127 | "chai-http": { 128 | "version": "3.0.0", 129 | "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-3.0.0.tgz", 130 | "integrity": "sha1-VGDYA24fGhKwtbXL1Snm3B0x60s=", 131 | "dev": true, 132 | "requires": { 133 | "cookiejar": "2.0.6", 134 | "is-ip": "1.0.0", 135 | "methods": "1.1.2", 136 | "qs": "6.5.1", 137 | "superagent": "2.3.0" 138 | } 139 | }, 140 | "check-error": { 141 | "version": "1.0.2", 142 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 143 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 144 | "dev": true 145 | }, 146 | "combined-stream": { 147 | "version": "1.0.5", 148 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", 149 | "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", 150 | "dev": true, 151 | "requires": { 152 | "delayed-stream": "1.0.0" 153 | } 154 | }, 155 | "component-emitter": { 156 | "version": "1.2.1", 157 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", 158 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", 159 | "dev": true 160 | }, 161 | "concat-map": { 162 | "version": "0.0.1", 163 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 164 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 165 | "dev": true 166 | }, 167 | "content-disposition": { 168 | "version": "0.5.2", 169 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 170 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 171 | }, 172 | "content-type": { 173 | "version": "1.0.4", 174 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 175 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 176 | }, 177 | "cookie": { 178 | "version": "0.3.1", 179 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 180 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 181 | }, 182 | "cookie-signature": { 183 | "version": "1.0.6", 184 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 185 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 186 | }, 187 | "cookiejar": { 188 | "version": "2.0.6", 189 | "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.6.tgz", 190 | "integrity": "sha1-Cr81atANHFohnYjURRgEbdAmrP4=", 191 | "dev": true 192 | }, 193 | "core-util-is": { 194 | "version": "1.0.2", 195 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 196 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 197 | "dev": true 198 | }, 199 | "debug": { 200 | "version": "2.6.9", 201 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 202 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 203 | "requires": { 204 | "ms": "2.0.0" 205 | } 206 | }, 207 | "deep-eql": { 208 | "version": "3.0.1", 209 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 210 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 211 | "dev": true, 212 | "requires": { 213 | "type-detect": "4.0.5" 214 | } 215 | }, 216 | "delayed-stream": { 217 | "version": "1.0.0", 218 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 219 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 220 | "dev": true 221 | }, 222 | "depd": { 223 | "version": "1.1.1", 224 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 225 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 226 | }, 227 | "destroy": { 228 | "version": "1.0.4", 229 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 230 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 231 | }, 232 | "dotenv": { 233 | "version": "4.0.0", 234 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz", 235 | "integrity": "sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=" 236 | }, 237 | "ecdsa-sig-formatter": { 238 | "version": "1.0.9", 239 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", 240 | "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", 241 | "requires": { 242 | "base64url": "2.0.0", 243 | "safe-buffer": "5.1.1" 244 | } 245 | }, 246 | "ee-first": { 247 | "version": "1.1.1", 248 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 249 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 250 | }, 251 | "encodeurl": { 252 | "version": "1.0.1", 253 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", 254 | "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" 255 | }, 256 | "escape-html": { 257 | "version": "1.0.3", 258 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 259 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 260 | }, 261 | "escape-string-regexp": { 262 | "version": "1.0.5", 263 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 264 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 265 | "dev": true 266 | }, 267 | "etag": { 268 | "version": "1.8.1", 269 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 270 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 271 | }, 272 | "express": { 273 | "version": "4.16.2", 274 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", 275 | "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", 276 | "requires": { 277 | "accepts": "1.3.4", 278 | "array-flatten": "1.1.1", 279 | "body-parser": "1.18.2", 280 | "content-disposition": "0.5.2", 281 | "content-type": "1.0.4", 282 | "cookie": "0.3.1", 283 | "cookie-signature": "1.0.6", 284 | "debug": "2.6.9", 285 | "depd": "1.1.1", 286 | "encodeurl": "1.0.1", 287 | "escape-html": "1.0.3", 288 | "etag": "1.8.1", 289 | "finalhandler": "1.1.0", 290 | "fresh": "0.5.2", 291 | "merge-descriptors": "1.0.1", 292 | "methods": "1.1.2", 293 | "on-finished": "2.3.0", 294 | "parseurl": "1.3.2", 295 | "path-to-regexp": "0.1.7", 296 | "proxy-addr": "2.0.2", 297 | "qs": "6.5.1", 298 | "range-parser": "1.2.0", 299 | "safe-buffer": "5.1.1", 300 | "send": "0.16.1", 301 | "serve-static": "1.13.1", 302 | "setprototypeof": "1.1.0", 303 | "statuses": "1.3.1", 304 | "type-is": "1.6.15", 305 | "utils-merge": "1.0.1", 306 | "vary": "1.1.2" 307 | }, 308 | "dependencies": { 309 | "setprototypeof": { 310 | "version": "1.1.0", 311 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 312 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 313 | }, 314 | "statuses": { 315 | "version": "1.3.1", 316 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 317 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 318 | } 319 | } 320 | }, 321 | "extend": { 322 | "version": "3.0.1", 323 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", 324 | "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", 325 | "dev": true 326 | }, 327 | "faker": { 328 | "version": "3.1.0", 329 | "resolved": "https://registry.npmjs.org/faker/-/faker-3.1.0.tgz", 330 | "integrity": "sha1-D5CPr05uwCUk5UpX5DLFwBPgjJ8=", 331 | "dev": true 332 | }, 333 | "finalhandler": { 334 | "version": "1.1.0", 335 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", 336 | "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", 337 | "requires": { 338 | "debug": "2.6.9", 339 | "encodeurl": "1.0.1", 340 | "escape-html": "1.0.3", 341 | "on-finished": "2.3.0", 342 | "parseurl": "1.3.2", 343 | "statuses": "1.3.1", 344 | "unpipe": "1.0.0" 345 | }, 346 | "dependencies": { 347 | "statuses": { 348 | "version": "1.3.1", 349 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 350 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 351 | } 352 | } 353 | }, 354 | "form-data": { 355 | "version": "1.0.0-rc4", 356 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", 357 | "integrity": "sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14=", 358 | "dev": true, 359 | "requires": { 360 | "async": "1.5.2", 361 | "combined-stream": "1.0.5", 362 | "mime-types": "2.1.17" 363 | }, 364 | "dependencies": { 365 | "async": { 366 | "version": "1.5.2", 367 | "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", 368 | "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", 369 | "dev": true 370 | } 371 | } 372 | }, 373 | "formidable": { 374 | "version": "1.1.1", 375 | "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.1.1.tgz", 376 | "integrity": "sha1-lriIb3w8NQi5Mta9cMTTqI818ak=", 377 | "dev": true 378 | }, 379 | "forwarded": { 380 | "version": "0.1.2", 381 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 382 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 383 | }, 384 | "fresh": { 385 | "version": "0.5.2", 386 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 387 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 388 | }, 389 | "fs.realpath": { 390 | "version": "1.0.0", 391 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 392 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 393 | "dev": true 394 | }, 395 | "get-func-name": { 396 | "version": "2.0.0", 397 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 398 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 399 | "dev": true 400 | }, 401 | "he": { 402 | "version": "1.1.1", 403 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 404 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 405 | "dev": true 406 | }, 407 | "hoek": { 408 | "version": "2.16.3", 409 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", 410 | "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" 411 | }, 412 | "http-errors": { 413 | "version": "1.6.2", 414 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 415 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 416 | "requires": { 417 | "depd": "1.1.1", 418 | "inherits": "2.0.3", 419 | "setprototypeof": "1.0.3", 420 | "statuses": "1.4.0" 421 | } 422 | }, 423 | "iconv-lite": { 424 | "version": "0.4.19", 425 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 426 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 427 | }, 428 | "inflight": { 429 | "version": "1.0.6", 430 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 431 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 432 | "dev": true, 433 | "requires": { 434 | "once": "1.4.0", 435 | "wrappy": "1.0.2" 436 | } 437 | }, 438 | "inherits": { 439 | "version": "2.0.3", 440 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 441 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 442 | }, 443 | "ip-regex": { 444 | "version": "1.0.3", 445 | "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-1.0.3.tgz", 446 | "integrity": "sha1-3FiQdvZZ9BnCIgOaMzFvHHOH7/0=", 447 | "dev": true 448 | }, 449 | "ipaddr.js": { 450 | "version": "1.5.2", 451 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", 452 | "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" 453 | }, 454 | "is-ip": { 455 | "version": "1.0.0", 456 | "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-1.0.0.tgz", 457 | "integrity": "sha1-K7aVn3l8zW+f3IEnWLy8h8TFkHQ=", 458 | "dev": true, 459 | "requires": { 460 | "ip-regex": "1.0.3" 461 | } 462 | }, 463 | "isarray": { 464 | "version": "1.0.0", 465 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 466 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 467 | "dev": true 468 | }, 469 | "isemail": { 470 | "version": "1.2.0", 471 | "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", 472 | "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" 473 | }, 474 | "joi": { 475 | "version": "6.10.1", 476 | "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", 477 | "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", 478 | "requires": { 479 | "hoek": "2.16.3", 480 | "isemail": "1.2.0", 481 | "moment": "2.21.0", 482 | "topo": "1.1.0" 483 | } 484 | }, 485 | "jsonwebtoken": { 486 | "version": "8.2.0", 487 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.2.0.tgz", 488 | "integrity": "sha512-1Wxh8ADP3cNyPl8tZ95WtraHXCAyXupgc0AhMHjU9er98BV+UcKsO7OJUjfhIu0Uba9A40n1oSx8dbJYrm+EoQ==", 489 | "requires": { 490 | "jws": "3.1.4", 491 | "lodash.includes": "4.3.0", 492 | "lodash.isboolean": "3.0.3", 493 | "lodash.isinteger": "4.0.4", 494 | "lodash.isnumber": "3.0.3", 495 | "lodash.isplainobject": "4.0.6", 496 | "lodash.isstring": "4.0.1", 497 | "lodash.once": "4.1.1", 498 | "ms": "2.1.1", 499 | "xtend": "4.0.1" 500 | }, 501 | "dependencies": { 502 | "ms": { 503 | "version": "2.1.1", 504 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 505 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 506 | } 507 | } 508 | }, 509 | "jwa": { 510 | "version": "1.1.5", 511 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", 512 | "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", 513 | "requires": { 514 | "base64url": "2.0.0", 515 | "buffer-equal-constant-time": "1.0.1", 516 | "ecdsa-sig-formatter": "1.0.9", 517 | "safe-buffer": "5.1.1" 518 | } 519 | }, 520 | "jws": { 521 | "version": "3.1.4", 522 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", 523 | "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", 524 | "requires": { 525 | "base64url": "2.0.0", 526 | "jwa": "1.1.5", 527 | "safe-buffer": "5.1.1" 528 | } 529 | }, 530 | "lodash": { 531 | "version": "4.17.4", 532 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 533 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 534 | }, 535 | "lodash.get": { 536 | "version": "4.4.2", 537 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 538 | "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" 539 | }, 540 | "lodash.includes": { 541 | "version": "4.3.0", 542 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 543 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 544 | }, 545 | "lodash.isboolean": { 546 | "version": "3.0.3", 547 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 548 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 549 | }, 550 | "lodash.isinteger": { 551 | "version": "4.0.4", 552 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 553 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 554 | }, 555 | "lodash.isnumber": { 556 | "version": "3.0.3", 557 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 558 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 559 | }, 560 | "lodash.isplainobject": { 561 | "version": "4.0.6", 562 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 563 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 564 | }, 565 | "lodash.isstring": { 566 | "version": "4.0.1", 567 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 568 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 569 | }, 570 | "lodash.once": { 571 | "version": "4.1.1", 572 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 573 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 574 | }, 575 | "media-typer": { 576 | "version": "0.3.0", 577 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 578 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 579 | }, 580 | "merge-descriptors": { 581 | "version": "1.0.1", 582 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 583 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 584 | }, 585 | "methods": { 586 | "version": "1.1.2", 587 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 588 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 589 | }, 590 | "mime": { 591 | "version": "1.4.1", 592 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 593 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 594 | }, 595 | "mime-db": { 596 | "version": "1.30.0", 597 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", 598 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" 599 | }, 600 | "mime-types": { 601 | "version": "2.1.17", 602 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", 603 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", 604 | "requires": { 605 | "mime-db": "1.30.0" 606 | } 607 | }, 608 | "minimatch": { 609 | "version": "3.0.4", 610 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 611 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 612 | "dev": true, 613 | "requires": { 614 | "brace-expansion": "1.1.8" 615 | } 616 | }, 617 | "minimist": { 618 | "version": "0.0.8", 619 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 620 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 621 | "dev": true 622 | }, 623 | "mkdirp": { 624 | "version": "0.5.1", 625 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 626 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 627 | "dev": true, 628 | "requires": { 629 | "minimist": "0.0.8" 630 | } 631 | }, 632 | "mocha": { 633 | "version": "4.1.0", 634 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", 635 | "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", 636 | "dev": true, 637 | "requires": { 638 | "browser-stdout": "1.3.0", 639 | "commander": "2.11.0", 640 | "debug": "3.1.0", 641 | "diff": "3.3.1", 642 | "escape-string-regexp": "1.0.5", 643 | "glob": "7.1.2", 644 | "growl": "1.10.3", 645 | "he": "1.1.1", 646 | "mkdirp": "0.5.1", 647 | "supports-color": "4.4.0" 648 | }, 649 | "dependencies": { 650 | "commander": { 651 | "version": "2.11.0", 652 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", 653 | "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", 654 | "dev": true 655 | }, 656 | "debug": { 657 | "version": "3.1.0", 658 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 659 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 660 | "dev": true, 661 | "requires": { 662 | "ms": "2.0.0" 663 | } 664 | }, 665 | "diff": { 666 | "version": "3.3.1", 667 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", 668 | "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", 669 | "dev": true 670 | }, 671 | "glob": { 672 | "version": "7.1.2", 673 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 674 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 675 | "dev": true, 676 | "requires": { 677 | "fs.realpath": "1.0.0", 678 | "inflight": "1.0.6", 679 | "inherits": "2.0.3", 680 | "minimatch": "3.0.4", 681 | "once": "1.4.0", 682 | "path-is-absolute": "1.0.1" 683 | } 684 | }, 685 | "growl": { 686 | "version": "1.10.3", 687 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", 688 | "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", 689 | "dev": true 690 | }, 691 | "has-flag": { 692 | "version": "2.0.0", 693 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", 694 | "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", 695 | "dev": true 696 | }, 697 | "supports-color": { 698 | "version": "4.4.0", 699 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", 700 | "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", 701 | "dev": true, 702 | "requires": { 703 | "has-flag": "2.0.0" 704 | } 705 | } 706 | } 707 | }, 708 | "moment": { 709 | "version": "2.21.0", 710 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz", 711 | "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" 712 | }, 713 | "mongoose": { 714 | "version": "5.0.10", 715 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.0.10.tgz", 716 | "integrity": "sha512-vBfFP6hOHBdsWogc84cLofclWVAiu0+q0/oLxL/y61RUpW4K3BIGH2QhI+7lPBrGpGS1Yk/KfnumndWQI7wZiA==", 717 | "requires": { 718 | "async": "2.1.4", 719 | "bson": "1.0.4", 720 | "kareem": "2.0.5", 721 | "lodash.get": "4.4.2", 722 | "mongodb": "3.0.4", 723 | "mongoose-legacy-pluralize": "1.0.2", 724 | "mpath": "0.3.0", 725 | "mquery": "3.0.0", 726 | "ms": "2.0.0", 727 | "regexp-clone": "0.0.1", 728 | "sliced": "1.0.1" 729 | }, 730 | "dependencies": { 731 | "bluebird": { 732 | "version": "3.5.0", 733 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", 734 | "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" 735 | }, 736 | "kareem": { 737 | "version": "2.0.5", 738 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.0.5.tgz", 739 | "integrity": "sha512-dfvpj3mCGJLZuADInhYrKaXkGarJxDqnTEiF91wK6fqwdCRmN+O4aEp8575UjZlQzDkzLI1WDL1uU7vyupURqw==" 740 | }, 741 | "mongodb": { 742 | "version": "3.0.4", 743 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.0.4.tgz", 744 | "integrity": "sha512-90YIIs7A4ko4kCGafxxXj3foexCAlJBC0YLwwIKgSLoE7Vni2IqUMz6HSsZ3zbXOfR1KWtxfnc0RyAMAY/ViLg==", 745 | "requires": { 746 | "mongodb-core": "3.0.4" 747 | } 748 | }, 749 | "mongodb-core": { 750 | "version": "3.0.4", 751 | "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.0.4.tgz", 752 | "integrity": "sha512-OTH267FjfwBdEufSnrgd+u8HuLWRuQ6p8DR0XirPl2BdlLEMh4XwjJf1RTlruILp5p2m1w8dDC8rCxibC3W8qQ==", 753 | "requires": { 754 | "bson": "1.0.4", 755 | "require_optional": "1.0.1" 756 | } 757 | }, 758 | "mquery": { 759 | "version": "3.0.0", 760 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.0.0.tgz", 761 | "integrity": "sha512-WL1Lk8v4l8VFSSwN3yCzY9TXw+fKVYKn6f+w86TRzOLSE8k1yTgGaLBPUByJQi8VcLbOdnUneFV/y3Kv874pnQ==", 762 | "requires": { 763 | "bluebird": "3.5.0", 764 | "debug": "2.6.9", 765 | "regexp-clone": "0.0.1", 766 | "sliced": "0.0.5" 767 | }, 768 | "dependencies": { 769 | "sliced": { 770 | "version": "0.0.5", 771 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", 772 | "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" 773 | } 774 | } 775 | } 776 | } 777 | }, 778 | "mongoose-legacy-pluralize": { 779 | "version": "1.0.2", 780 | "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", 781 | "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" 782 | }, 783 | "morgan": { 784 | "version": "1.9.0", 785 | "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", 786 | "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", 787 | "requires": { 788 | "basic-auth": "2.0.0", 789 | "debug": "2.6.9", 790 | "depd": "1.1.1", 791 | "on-finished": "2.3.0", 792 | "on-headers": "1.0.1" 793 | } 794 | }, 795 | "mpath": { 796 | "version": "0.3.0", 797 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz", 798 | "integrity": "sha1-elj3iem1/TyUUgY0FXlg8mvV70Q=" 799 | }, 800 | "ms": { 801 | "version": "2.0.0", 802 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 803 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 804 | }, 805 | "negotiator": { 806 | "version": "0.6.1", 807 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 808 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 809 | }, 810 | "on-finished": { 811 | "version": "2.3.0", 812 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 813 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 814 | "requires": { 815 | "ee-first": "1.1.1" 816 | } 817 | }, 818 | "on-headers": { 819 | "version": "1.0.1", 820 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", 821 | "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" 822 | }, 823 | "once": { 824 | "version": "1.4.0", 825 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 826 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 827 | "dev": true, 828 | "requires": { 829 | "wrappy": "1.0.2" 830 | } 831 | }, 832 | "parseurl": { 833 | "version": "1.3.2", 834 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 835 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 836 | }, 837 | "passport": { 838 | "version": "0.3.2", 839 | "resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz", 840 | "integrity": "sha1-ndAJ+RXo/glbASSgG4+C2gdRAQI=", 841 | "requires": { 842 | "passport-strategy": "1.0.0", 843 | "pause": "0.0.1" 844 | } 845 | }, 846 | "passport-jwt": { 847 | "version": "2.2.1", 848 | "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-2.2.1.tgz", 849 | "integrity": "sha1-DgBMlAcTGdZz2dm8/RV0qGgBFSc=", 850 | "requires": { 851 | "jsonwebtoken": "7.4.3", 852 | "passport-strategy": "1.0.0" 853 | }, 854 | "dependencies": { 855 | "jsonwebtoken": { 856 | "version": "7.4.3", 857 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.3.tgz", 858 | "integrity": "sha1-d/UCHeBYtgWheD+hKD6ZgS5kVjg=", 859 | "requires": { 860 | "joi": "6.10.1", 861 | "jws": "3.1.4", 862 | "lodash.once": "4.1.1", 863 | "ms": "2.0.0", 864 | "xtend": "4.0.1" 865 | } 866 | } 867 | } 868 | }, 869 | "passport-local": { 870 | "version": "1.0.0", 871 | "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", 872 | "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", 873 | "requires": { 874 | "passport-strategy": "1.0.0" 875 | } 876 | }, 877 | "passport-strategy": { 878 | "version": "1.0.0", 879 | "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", 880 | "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" 881 | }, 882 | "path-is-absolute": { 883 | "version": "1.0.1", 884 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 885 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 886 | "dev": true 887 | }, 888 | "path-to-regexp": { 889 | "version": "0.1.7", 890 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 891 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 892 | }, 893 | "pathval": { 894 | "version": "1.1.0", 895 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 896 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 897 | "dev": true 898 | }, 899 | "pause": { 900 | "version": "0.0.1", 901 | "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", 902 | "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" 903 | }, 904 | "process-nextick-args": { 905 | "version": "1.0.7", 906 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", 907 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", 908 | "dev": true 909 | }, 910 | "proxy-addr": { 911 | "version": "2.0.2", 912 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", 913 | "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", 914 | "requires": { 915 | "forwarded": "0.1.2", 916 | "ipaddr.js": "1.5.2" 917 | } 918 | }, 919 | "qs": { 920 | "version": "6.5.1", 921 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 922 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 923 | }, 924 | "range-parser": { 925 | "version": "1.2.0", 926 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 927 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 928 | }, 929 | "raw-body": { 930 | "version": "2.3.2", 931 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 932 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 933 | "requires": { 934 | "bytes": "3.0.0", 935 | "http-errors": "1.6.2", 936 | "iconv-lite": "0.4.19", 937 | "unpipe": "1.0.0" 938 | } 939 | }, 940 | "readable-stream": { 941 | "version": "2.2.7", 942 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", 943 | "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", 944 | "dev": true, 945 | "requires": { 946 | "buffer-shims": "1.0.0", 947 | "core-util-is": "1.0.2", 948 | "inherits": "2.0.3", 949 | "isarray": "1.0.0", 950 | "process-nextick-args": "1.0.7", 951 | "string_decoder": "1.0.3", 952 | "util-deprecate": "1.0.2" 953 | } 954 | }, 955 | "regexp-clone": { 956 | "version": "0.0.1", 957 | "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", 958 | "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" 959 | }, 960 | "require_optional": { 961 | "version": "1.0.1", 962 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", 963 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", 964 | "requires": { 965 | "resolve-from": "2.0.0", 966 | "semver": "5.4.1" 967 | } 968 | }, 969 | "resolve-from": { 970 | "version": "2.0.0", 971 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", 972 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" 973 | }, 974 | "safe-buffer": { 975 | "version": "5.1.1", 976 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 977 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 978 | }, 979 | "semver": { 980 | "version": "5.4.1", 981 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", 982 | "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" 983 | }, 984 | "send": { 985 | "version": "0.16.1", 986 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", 987 | "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", 988 | "requires": { 989 | "debug": "2.6.9", 990 | "depd": "1.1.1", 991 | "destroy": "1.0.4", 992 | "encodeurl": "1.0.1", 993 | "escape-html": "1.0.3", 994 | "etag": "1.8.1", 995 | "fresh": "0.5.2", 996 | "http-errors": "1.6.2", 997 | "mime": "1.4.1", 998 | "ms": "2.0.0", 999 | "on-finished": "2.3.0", 1000 | "range-parser": "1.2.0", 1001 | "statuses": "1.3.1" 1002 | }, 1003 | "dependencies": { 1004 | "statuses": { 1005 | "version": "1.3.1", 1006 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 1007 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 1008 | } 1009 | } 1010 | }, 1011 | "serve-static": { 1012 | "version": "1.13.1", 1013 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", 1014 | "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", 1015 | "requires": { 1016 | "encodeurl": "1.0.1", 1017 | "escape-html": "1.0.3", 1018 | "parseurl": "1.3.2", 1019 | "send": "0.16.1" 1020 | } 1021 | }, 1022 | "setprototypeof": { 1023 | "version": "1.0.3", 1024 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 1025 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 1026 | }, 1027 | "sliced": { 1028 | "version": "1.0.1", 1029 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", 1030 | "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" 1031 | }, 1032 | "statuses": { 1033 | "version": "1.4.0", 1034 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 1035 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 1036 | }, 1037 | "string_decoder": { 1038 | "version": "1.0.3", 1039 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", 1040 | "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", 1041 | "dev": true, 1042 | "requires": { 1043 | "safe-buffer": "5.1.1" 1044 | } 1045 | }, 1046 | "superagent": { 1047 | "version": "2.3.0", 1048 | "resolved": "https://registry.npmjs.org/superagent/-/superagent-2.3.0.tgz", 1049 | "integrity": "sha1-cDUpoHFOV+EjlZ3e+84ZOy5Q0RU=", 1050 | "dev": true, 1051 | "requires": { 1052 | "component-emitter": "1.2.1", 1053 | "cookiejar": "2.0.6", 1054 | "debug": "2.6.9", 1055 | "extend": "3.0.1", 1056 | "form-data": "1.0.0-rc4", 1057 | "formidable": "1.1.1", 1058 | "methods": "1.1.2", 1059 | "mime": "1.4.1", 1060 | "qs": "6.5.1", 1061 | "readable-stream": "2.2.7" 1062 | } 1063 | }, 1064 | "topo": { 1065 | "version": "1.1.0", 1066 | "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", 1067 | "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", 1068 | "requires": { 1069 | "hoek": "2.16.3" 1070 | } 1071 | }, 1072 | "type-detect": { 1073 | "version": "4.0.5", 1074 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.5.tgz", 1075 | "integrity": "sha512-N9IvkQslUGYGC24RkJk1ba99foK6TkwC2FHAEBlQFBP0RxQZS8ZpJuAZcwiY/w9ZJHFQb1aOXBI60OdxhTrwEQ==", 1076 | "dev": true 1077 | }, 1078 | "type-is": { 1079 | "version": "1.6.15", 1080 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", 1081 | "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", 1082 | "requires": { 1083 | "media-typer": "0.3.0", 1084 | "mime-types": "2.1.17" 1085 | } 1086 | }, 1087 | "unpipe": { 1088 | "version": "1.0.0", 1089 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1090 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1091 | }, 1092 | "util-deprecate": { 1093 | "version": "1.0.2", 1094 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1095 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 1096 | "dev": true 1097 | }, 1098 | "utils-merge": { 1099 | "version": "1.0.1", 1100 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1101 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1102 | }, 1103 | "vary": { 1104 | "version": "1.1.2", 1105 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1106 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1107 | }, 1108 | "wrappy": { 1109 | "version": "1.0.2", 1110 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1111 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1112 | "dev": true 1113 | }, 1114 | "xtend": { 1115 | "version": "4.0.1", 1116 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 1117 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" 1118 | } 1119 | } 1120 | } 1121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-jwt-auth", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "mocha --exit" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcryptjs": "^2.4.0", 15 | "body-parser": "^1.15.2", 16 | "dotenv": "^4.0.0", 17 | "express": "^4.14.0", 18 | "jsonwebtoken": "^8.2.0", 19 | "mongoose": "^5.0.6", 20 | "morgan": "^1.7.0", 21 | "passport": "^0.3.2", 22 | "passport-jwt": "^2.2.1", 23 | "passport-local": "^1.0.0" 24 | }, 25 | "devDependencies": { 26 | "chai": "^4.0.1", 27 | "chai-http": "^3.0.0", 28 | "faker": "^3.1.0", 29 | "mocha": "^4.0.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('dotenv').config(); 3 | const express = require('express'); 4 | const mongoose = require('mongoose'); 5 | const morgan = require('morgan'); 6 | const passport = require('passport'); 7 | 8 | // Here we use destructuring assignment with renaming so the two variables 9 | // called router (from ./users and ./auth) have different names 10 | // For example: 11 | // const actorSurnames = { james: "Stewart", robert: "De Niro" }; 12 | // const { james: jimmy, robert: bobby } = actorSurnames; 13 | // console.log(jimmy); // Stewart - the variable name is jimmy, not james 14 | // console.log(bobby); // De Niro - the variable name is bobby, not robert 15 | const { router: usersRouter } = require('./users'); 16 | const { router: authRouter, localStrategy, jwtStrategy } = require('./auth'); 17 | 18 | mongoose.Promise = global.Promise; 19 | 20 | const { PORT, DATABASE_URL } = require('./config'); 21 | 22 | const app = express(); 23 | 24 | // Logging 25 | app.use(morgan('common')); 26 | 27 | // CORS 28 | app.use(function (req, res, next) { 29 | res.header('Access-Control-Allow-Origin', '*'); 30 | res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization'); 31 | res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE'); 32 | if (req.method === 'OPTIONS') { 33 | return res.send(204); 34 | } 35 | next(); 36 | }); 37 | 38 | passport.use(localStrategy); 39 | passport.use(jwtStrategy); 40 | 41 | app.use('/api/users/', usersRouter); 42 | app.use('/api/auth/', authRouter); 43 | 44 | const jwtAuth = passport.authenticate('jwt', { session: false }); 45 | 46 | // A protected endpoint which needs a valid JWT to access it 47 | app.get('/api/protected', jwtAuth, (req, res) => { 48 | return res.json({ 49 | data: 'rosebud' 50 | }); 51 | }); 52 | 53 | app.use('*', (req, res) => { 54 | return res.status(404).json({ message: 'Not Found' }); 55 | }); 56 | 57 | // Referenced by both runServer and closeServer. closeServer 58 | // assumes runServer has run and set `server` to a server object 59 | let server; 60 | 61 | function runServer(databaseUrl, port = PORT) { 62 | 63 | return new Promise((resolve, reject) => { 64 | mongoose.connect(databaseUrl, err => { 65 | if (err) { 66 | return reject(err); 67 | } 68 | server = app.listen(port, () => { 69 | console.log(`Your app is listening on port ${port}`); 70 | resolve(); 71 | }) 72 | .on('error', err => { 73 | mongoose.disconnect(); 74 | reject(err); 75 | }); 76 | }); 77 | }); 78 | } 79 | 80 | function closeServer() { 81 | return mongoose.disconnect().then(() => { 82 | return new Promise((resolve, reject) => { 83 | console.log('Closing server'); 84 | server.close(err => { 85 | if (err) { 86 | return reject(err); 87 | } 88 | resolve(); 89 | }); 90 | }); 91 | }); 92 | } 93 | 94 | if (require.main === module) { 95 | runServer(DATABASE_URL).catch(err => console.error(err)); 96 | } 97 | 98 | module.exports = { app, runServer, closeServer }; 99 | -------------------------------------------------------------------------------- /test/test-auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | const jwt = require('jsonwebtoken'); 6 | 7 | const { app, runServer, closeServer } = require('../server'); 8 | const { User } = require('../users'); 9 | const { JWT_SECRET, TEST_DATABASE_URL } = require('../config'); 10 | 11 | const expect = chai.expect; 12 | 13 | // This let's us make HTTP requests 14 | // in our tests. 15 | // see: https://github.com/chaijs/chai-http 16 | chai.use(chaiHttp); 17 | 18 | describe('Auth endpoints', function () { 19 | const username = 'exampleUser'; 20 | const password = 'examplePass'; 21 | const firstName = 'Example'; 22 | const lastName = 'User'; 23 | 24 | before(function () { 25 | return runServer(TEST_DATABASE_URL); 26 | }); 27 | 28 | after(function () { 29 | return closeServer(); 30 | }); 31 | 32 | beforeEach(function () { 33 | return User.hashPassword(password).then(password => 34 | User.create({ 35 | username, 36 | password, 37 | firstName, 38 | lastName 39 | }) 40 | ); 41 | }); 42 | 43 | afterEach(function () { 44 | return User.remove({}); 45 | }); 46 | 47 | describe('/api/auth/login', function () { 48 | it('Should reject requests with no credentials', function () { 49 | return chai 50 | .request(app) 51 | .post('/api/auth/login') 52 | .then(() => 53 | expect.fail(null, null, 'Request should not succeed') 54 | ) 55 | .catch(err => { 56 | if (err instanceof chai.AssertionError) { 57 | throw err; 58 | } 59 | 60 | const res = err.response; 61 | expect(res).to.have.status(400); 62 | }); 63 | }); 64 | it('Should reject requests with incorrect usernames', function () { 65 | return chai 66 | .request(app) 67 | .post('/api/auth/login') 68 | .send({ username: 'wrongUsername', password }) 69 | .then(() => 70 | expect.fail(null, null, 'Request should not succeed') 71 | ) 72 | .catch(err => { 73 | if (err instanceof chai.AssertionError) { 74 | throw err; 75 | } 76 | 77 | const res = err.response; 78 | expect(res).to.have.status(401); 79 | }); 80 | }); 81 | it('Should reject requests with incorrect passwords', function () { 82 | return chai 83 | .request(app) 84 | .post('/api/auth/login') 85 | .send({ username, password: 'wrongPassword' }) 86 | .then(() => 87 | expect.fail(null, null, 'Request should not succeed') 88 | ) 89 | .catch(err => { 90 | if (err instanceof chai.AssertionError) { 91 | throw err; 92 | } 93 | 94 | const res = err.response; 95 | expect(res).to.have.status(401); 96 | }); 97 | }); 98 | it('Should return a valid auth token', function () { 99 | return chai 100 | .request(app) 101 | .post('/api/auth/login') 102 | .send({ username, password }) 103 | .then(res => { 104 | expect(res).to.have.status(200); 105 | expect(res.body).to.be.an('object'); 106 | const token = res.body.authToken; 107 | expect(token).to.be.a('string'); 108 | const payload = jwt.verify(token, JWT_SECRET, { 109 | algorithm: ['HS256'] 110 | }); 111 | expect(payload.user).to.deep.equal({ 112 | username, 113 | firstName, 114 | lastName 115 | }); 116 | }); 117 | }); 118 | }); 119 | 120 | describe('/api/auth/refresh', function () { 121 | it('Should reject requests with no credentials', function () { 122 | return chai 123 | .request(app) 124 | .post('/api/auth/refresh') 125 | .then(() => 126 | expect.fail(null, null, 'Request should not succeed') 127 | ) 128 | .catch(err => { 129 | if (err instanceof chai.AssertionError) { 130 | throw err; 131 | } 132 | 133 | const res = err.response; 134 | expect(res).to.have.status(401); 135 | }); 136 | }); 137 | it('Should reject requests with an invalid token', function () { 138 | const token = jwt.sign( 139 | { 140 | username, 141 | firstName, 142 | lastName 143 | }, 144 | 'wrongSecret', 145 | { 146 | algorithm: 'HS256', 147 | expiresIn: '7d' 148 | } 149 | ); 150 | 151 | return chai 152 | .request(app) 153 | .post('/api/auth/refresh') 154 | .set('Authorization', `Bearer ${token}`) 155 | .then(() => 156 | expect.fail(null, null, 'Request should not succeed') 157 | ) 158 | .catch(err => { 159 | if (err instanceof chai.AssertionError) { 160 | throw err; 161 | } 162 | 163 | const res = err.response; 164 | expect(res).to.have.status(401); 165 | }); 166 | }); 167 | it('Should reject requests with an expired token', function () { 168 | const token = jwt.sign( 169 | { 170 | user: { 171 | username, 172 | firstName, 173 | lastName 174 | }, 175 | exp: Math.floor(Date.now() / 1000) - 10 // Expired ten seconds ago 176 | }, 177 | JWT_SECRET, 178 | { 179 | algorithm: 'HS256', 180 | subject: username 181 | } 182 | ); 183 | 184 | return chai 185 | .request(app) 186 | .post('/api/auth/refresh') 187 | .set('authorization', `Bearer ${token}`) 188 | .then(() => 189 | expect.fail(null, null, 'Request should not succeed') 190 | ) 191 | .catch(err => { 192 | if (err instanceof chai.AssertionError) { 193 | throw err; 194 | } 195 | 196 | const res = err.response; 197 | expect(res).to.have.status(401); 198 | }); 199 | }); 200 | it('Should return a valid auth token with a newer expiry date', function () { 201 | const token = jwt.sign( 202 | { 203 | user: { 204 | username, 205 | firstName, 206 | lastName 207 | } 208 | }, 209 | JWT_SECRET, 210 | { 211 | algorithm: 'HS256', 212 | subject: username, 213 | expiresIn: '7d' 214 | } 215 | ); 216 | const decoded = jwt.decode(token); 217 | 218 | return chai 219 | .request(app) 220 | .post('/api/auth/refresh') 221 | .set('authorization', `Bearer ${token}`) 222 | .then(res => { 223 | expect(res).to.have.status(200); 224 | expect(res.body).to.be.an('object'); 225 | const token = res.body.authToken; 226 | expect(token).to.be.a('string'); 227 | const payload = jwt.verify(token, JWT_SECRET, { 228 | algorithm: ['HS256'] 229 | }); 230 | expect(payload.user).to.deep.equal({ 231 | username, 232 | firstName, 233 | lastName 234 | }); 235 | expect(payload.exp).to.be.at.least(decoded.exp); 236 | }); 237 | }); 238 | }); 239 | }); 240 | -------------------------------------------------------------------------------- /test/test-protected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | const jwt = require('jsonwebtoken'); 6 | 7 | const { app, runServer, closeServer } = require('../server'); 8 | const { User } = require('../users'); 9 | const { JWT_SECRET, TEST_DATABASE_URL } = require('../config'); 10 | 11 | const expect = chai.expect; 12 | 13 | // This let's us make HTTP requests 14 | // in our tests. 15 | // see: https://github.com/chaijs/chai-http 16 | chai.use(chaiHttp); 17 | 18 | describe('Protected endpoint', function () { 19 | const username = 'exampleUser'; 20 | const password = 'examplePass'; 21 | const firstName = 'Example'; 22 | const lastName = 'User'; 23 | 24 | before(function () { 25 | return runServer(TEST_DATABASE_URL); 26 | }); 27 | 28 | after(function () { 29 | return closeServer(); 30 | }); 31 | 32 | beforeEach(function () { 33 | return User.hashPassword(password).then(password => 34 | User.create({ 35 | username, 36 | password, 37 | firstName, 38 | lastName 39 | }) 40 | ); 41 | }); 42 | 43 | afterEach(function () { 44 | return User.remove({}); 45 | }); 46 | 47 | describe('/api/protected', function () { 48 | it('Should reject requests with no credentials', function () { 49 | return chai 50 | .request(app) 51 | .get('/api/protected') 52 | .then(() => 53 | expect.fail(null, null, 'Request should not succeed') 54 | ) 55 | .catch(err => { 56 | if (err instanceof chai.AssertionError) { 57 | throw err; 58 | } 59 | 60 | const res = err.response; 61 | expect(res).to.have.status(401); 62 | }); 63 | }); 64 | 65 | it('Should reject requests with an invalid token', function () { 66 | const token = jwt.sign( 67 | { 68 | username, 69 | firstName, 70 | lastName 71 | }, 72 | 'wrongSecret', 73 | { 74 | algorithm: 'HS256', 75 | expiresIn: '7d' 76 | } 77 | ); 78 | 79 | return chai 80 | .request(app) 81 | .get('/api/protected') 82 | .set('Authorization', `Bearer ${token}`) 83 | .then(() => 84 | expect.fail(null, null, 'Request should not succeed') 85 | ) 86 | .catch(err => { 87 | if (err instanceof chai.AssertionError) { 88 | throw err; 89 | } 90 | 91 | const res = err.response; 92 | expect(res).to.have.status(401); 93 | }); 94 | }); 95 | it('Should reject requests with an expired token', function () { 96 | const token = jwt.sign( 97 | { 98 | user: { 99 | username, 100 | firstName, 101 | lastName 102 | }, 103 | exp: Math.floor(Date.now() / 1000) - 10 // Expired ten seconds ago 104 | }, 105 | JWT_SECRET, 106 | { 107 | algorithm: 'HS256', 108 | subject: username 109 | } 110 | ); 111 | 112 | return chai 113 | .request(app) 114 | .get('/api/protected') 115 | .set('authorization', `Bearer ${token}`) 116 | .then(() => 117 | expect.fail(null, null, 'Request should not succeed') 118 | ) 119 | .catch(err => { 120 | if (err instanceof chai.AssertionError) { 121 | throw err; 122 | } 123 | 124 | const res = err.response; 125 | expect(res).to.have.status(401); 126 | }); 127 | }); 128 | it('Should send protected data', function () { 129 | const token = jwt.sign( 130 | { 131 | user: { 132 | username, 133 | firstName, 134 | lastName 135 | } 136 | }, 137 | JWT_SECRET, 138 | { 139 | algorithm: 'HS256', 140 | subject: username, 141 | expiresIn: '7d' 142 | } 143 | ); 144 | 145 | return chai 146 | .request(app) 147 | .get('/api/protected') 148 | .set('authorization', `Bearer ${token}`) 149 | .then(res => { 150 | expect(res).to.have.status(200); 151 | expect(res.body).to.be.an('object'); 152 | expect(res.body.data).to.equal('rosebud'); 153 | }); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /test/test-users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | 6 | const { app, runServer, closeServer } = require('../server'); 7 | const { User } = require('../users'); 8 | const { TEST_DATABASE_URL } = require('../config'); 9 | 10 | const expect = chai.expect; 11 | 12 | // This let's us make HTTP requests 13 | // in our tests. 14 | // see: https://github.com/chaijs/chai-http 15 | chai.use(chaiHttp); 16 | 17 | describe('/api/user', function () { 18 | const username = 'exampleUser'; 19 | const password = 'examplePass'; 20 | const firstName = 'Example'; 21 | const lastName = 'User'; 22 | const usernameB = 'exampleUserB'; 23 | const passwordB = 'examplePassB'; 24 | const firstNameB = 'ExampleB'; 25 | const lastNameB = 'UserB'; 26 | 27 | before(function () { 28 | return runServer(TEST_DATABASE_URL); 29 | }); 30 | 31 | after(function () { 32 | return closeServer(); 33 | }); 34 | 35 | beforeEach(function () { }); 36 | 37 | afterEach(function () { 38 | return User.remove({}); 39 | }); 40 | 41 | describe('/api/users', function () { 42 | describe('POST', function () { 43 | it('Should reject users with missing username', function () { 44 | return chai 45 | .request(app) 46 | .post('/api/users') 47 | .send({ 48 | password, 49 | firstName, 50 | lastName 51 | }) 52 | .then(() => 53 | expect.fail(null, null, 'Request should not succeed') 54 | ) 55 | .catch(err => { 56 | if (err instanceof chai.AssertionError) { 57 | throw err; 58 | } 59 | 60 | const res = err.response; 61 | expect(res).to.have.status(422); 62 | expect(res.body.reason).to.equal('ValidationError'); 63 | expect(res.body.message).to.equal('Missing field'); 64 | expect(res.body.location).to.equal('username'); 65 | }); 66 | }); 67 | it('Should reject users with missing password', function () { 68 | return chai 69 | .request(app) 70 | .post('/api/users') 71 | .send({ 72 | username, 73 | firstName, 74 | lastName 75 | }) 76 | .then(() => 77 | expect.fail(null, null, 'Request should not succeed') 78 | ) 79 | .catch(err => { 80 | if (err instanceof chai.AssertionError) { 81 | throw err; 82 | } 83 | 84 | const res = err.response; 85 | expect(res).to.have.status(422); 86 | expect(res.body.reason).to.equal('ValidationError'); 87 | expect(res.body.message).to.equal('Missing field'); 88 | expect(res.body.location).to.equal('password'); 89 | }); 90 | }); 91 | it('Should reject users with non-string username', function () { 92 | return chai 93 | .request(app) 94 | .post('/api/users') 95 | .send({ 96 | username: 1234, 97 | password, 98 | firstName, 99 | lastName 100 | }) 101 | .then(() => 102 | expect.fail(null, null, 'Request should not succeed') 103 | ) 104 | .catch(err => { 105 | if (err instanceof chai.AssertionError) { 106 | throw err; 107 | } 108 | 109 | const res = err.response; 110 | expect(res).to.have.status(422); 111 | expect(res.body.reason).to.equal('ValidationError'); 112 | expect(res.body.message).to.equal( 113 | 'Incorrect field type: expected string' 114 | ); 115 | expect(res.body.location).to.equal('username'); 116 | }); 117 | }); 118 | it('Should reject users with non-string password', function () { 119 | return chai 120 | .request(app) 121 | .post('/api/users') 122 | .send({ 123 | username, 124 | password: 1234, 125 | firstName, 126 | lastName 127 | }) 128 | .then(() => 129 | expect.fail(null, null, 'Request should not succeed') 130 | ) 131 | .catch(err => { 132 | if (err instanceof chai.AssertionError) { 133 | throw err; 134 | } 135 | 136 | const res = err.response; 137 | expect(res).to.have.status(422); 138 | expect(res.body.reason).to.equal('ValidationError'); 139 | expect(res.body.message).to.equal( 140 | 'Incorrect field type: expected string' 141 | ); 142 | expect(res.body.location).to.equal('password'); 143 | }); 144 | }); 145 | it('Should reject users with non-string first name', function () { 146 | return chai 147 | .request(app) 148 | .post('/api/users') 149 | .send({ 150 | username, 151 | password, 152 | firstName: 1234, 153 | lastName 154 | }) 155 | .then(() => 156 | expect.fail(null, null, 'Request should not succeed') 157 | ) 158 | .catch(err => { 159 | if (err instanceof chai.AssertionError) { 160 | throw err; 161 | } 162 | 163 | const res = err.response; 164 | expect(res).to.have.status(422); 165 | expect(res.body.reason).to.equal('ValidationError'); 166 | expect(res.body.message).to.equal( 167 | 'Incorrect field type: expected string' 168 | ); 169 | expect(res.body.location).to.equal('firstName'); 170 | }); 171 | }); 172 | it('Should reject users with non-string last name', function () { 173 | return chai 174 | .request(app) 175 | .post('/api/users') 176 | .send({ 177 | username, 178 | password, 179 | firstName, 180 | lastName: 1234 181 | }) 182 | .then(() => 183 | expect.fail(null, null, 'Request should not succeed') 184 | ) 185 | .catch(err => { 186 | if (err instanceof chai.AssertionError) { 187 | throw err; 188 | } 189 | 190 | const res = err.response; 191 | expect(res).to.have.status(422); 192 | expect(res.body.reason).to.equal('ValidationError'); 193 | expect(res.body.message).to.equal( 194 | 'Incorrect field type: expected string' 195 | ); 196 | expect(res.body.location).to.equal('lastName'); 197 | }); 198 | }); 199 | it('Should reject users with non-trimmed username', function () { 200 | return chai 201 | .request(app) 202 | .post('/api/users') 203 | .send({ 204 | username: ` ${username} `, 205 | password, 206 | firstName, 207 | lastName 208 | }) 209 | .then(() => 210 | expect.fail(null, null, 'Request should not succeed') 211 | ) 212 | .catch(err => { 213 | if (err instanceof chai.AssertionError) { 214 | throw err; 215 | } 216 | 217 | const res = err.response; 218 | expect(res).to.have.status(422); 219 | expect(res.body.reason).to.equal('ValidationError'); 220 | expect(res.body.message).to.equal( 221 | 'Cannot start or end with whitespace' 222 | ); 223 | expect(res.body.location).to.equal('username'); 224 | }); 225 | }); 226 | it('Should reject users with non-trimmed password', function () { 227 | return chai 228 | .request(app) 229 | .post('/api/users') 230 | .send({ 231 | username, 232 | password: ` ${password} `, 233 | firstName, 234 | lastName 235 | }) 236 | .then(() => 237 | expect.fail(null, null, 'Request should not succeed') 238 | ) 239 | .catch(err => { 240 | if (err instanceof chai.AssertionError) { 241 | throw err; 242 | } 243 | 244 | const res = err.response; 245 | expect(res).to.have.status(422); 246 | expect(res.body.reason).to.equal('ValidationError'); 247 | expect(res.body.message).to.equal( 248 | 'Cannot start or end with whitespace' 249 | ); 250 | expect(res.body.location).to.equal('password'); 251 | }); 252 | }); 253 | it('Should reject users with empty username', function () { 254 | return chai 255 | .request(app) 256 | .post('/api/users') 257 | .send({ 258 | username: '', 259 | password, 260 | firstName, 261 | lastName 262 | }) 263 | .then(() => 264 | expect.fail(null, null, 'Request should not succeed') 265 | ) 266 | .catch(err => { 267 | if (err instanceof chai.AssertionError) { 268 | throw err; 269 | } 270 | 271 | const res = err.response; 272 | expect(res).to.have.status(422); 273 | expect(res.body.reason).to.equal('ValidationError'); 274 | expect(res.body.message).to.equal( 275 | 'Must be at least 1 characters long' 276 | ); 277 | expect(res.body.location).to.equal('username'); 278 | }); 279 | }); 280 | it('Should reject users with password less than ten characters', function () { 281 | return chai 282 | .request(app) 283 | .post('/api/users') 284 | .send({ 285 | username, 286 | password: '123456789', 287 | firstName, 288 | lastName 289 | }) 290 | .then(() => 291 | expect.fail(null, null, 'Request should not succeed') 292 | ) 293 | .catch(err => { 294 | if (err instanceof chai.AssertionError) { 295 | throw err; 296 | } 297 | 298 | const res = err.response; 299 | expect(res).to.have.status(422); 300 | expect(res.body.reason).to.equal('ValidationError'); 301 | expect(res.body.message).to.equal( 302 | 'Must be at least 10 characters long' 303 | ); 304 | expect(res.body.location).to.equal('password'); 305 | }); 306 | }); 307 | it('Should reject users with password greater than 72 characters', function () { 308 | return chai 309 | .request(app) 310 | .post('/api/users') 311 | .send({ 312 | username, 313 | password: new Array(73).fill('a').join(''), 314 | firstName, 315 | lastName 316 | }) 317 | .then(() => 318 | expect.fail(null, null, 'Request should not succeed') 319 | ) 320 | .catch(err => { 321 | if (err instanceof chai.AssertionError) { 322 | throw err; 323 | } 324 | 325 | const res = err.response; 326 | expect(res).to.have.status(422); 327 | expect(res.body.reason).to.equal('ValidationError'); 328 | expect(res.body.message).to.equal( 329 | 'Must be at most 72 characters long' 330 | ); 331 | expect(res.body.location).to.equal('password'); 332 | }); 333 | }); 334 | it('Should reject users with duplicate username', function () { 335 | // Create an initial user 336 | return User.create({ 337 | username, 338 | password, 339 | firstName, 340 | lastName 341 | }) 342 | .then(() => 343 | // Try to create a second user with the same username 344 | chai.request(app).post('/api/users').send({ 345 | username, 346 | password, 347 | firstName, 348 | lastName 349 | }) 350 | ) 351 | .then(() => 352 | expect.fail(null, null, 'Request should not succeed') 353 | ) 354 | .catch(err => { 355 | if (err instanceof chai.AssertionError) { 356 | throw err; 357 | } 358 | 359 | const res = err.response; 360 | expect(res).to.have.status(422); 361 | expect(res.body.reason).to.equal('ValidationError'); 362 | expect(res.body.message).to.equal( 363 | 'Username already taken' 364 | ); 365 | expect(res.body.location).to.equal('username'); 366 | }); 367 | }); 368 | it('Should create a new user', function () { 369 | return chai 370 | .request(app) 371 | .post('/api/users') 372 | .send({ 373 | username, 374 | password, 375 | firstName, 376 | lastName 377 | }) 378 | .then(res => { 379 | expect(res).to.have.status(201); 380 | expect(res.body).to.be.an('object'); 381 | expect(res.body).to.have.keys( 382 | 'username', 383 | 'firstName', 384 | 'lastName' 385 | ); 386 | expect(res.body.username).to.equal(username); 387 | expect(res.body.firstName).to.equal(firstName); 388 | expect(res.body.lastName).to.equal(lastName); 389 | return User.findOne({ 390 | username 391 | }); 392 | }) 393 | .then(user => { 394 | expect(user).to.not.be.null; 395 | expect(user.firstName).to.equal(firstName); 396 | expect(user.lastName).to.equal(lastName); 397 | return user.validatePassword(password); 398 | }) 399 | .then(passwordIsCorrect => { 400 | expect(passwordIsCorrect).to.be.true; 401 | }); 402 | }); 403 | it('Should trim firstName and lastName', function () { 404 | return chai 405 | .request(app) 406 | .post('/api/users') 407 | .send({ 408 | username, 409 | password, 410 | firstName: ` ${firstName} `, 411 | lastName: ` ${lastName} ` 412 | }) 413 | .then(res => { 414 | expect(res).to.have.status(201); 415 | expect(res.body).to.be.an('object'); 416 | expect(res.body).to.have.keys( 417 | 'username', 418 | 'firstName', 419 | 'lastName' 420 | ); 421 | expect(res.body.username).to.equal(username); 422 | expect(res.body.firstName).to.equal(firstName); 423 | expect(res.body.lastName).to.equal(lastName); 424 | return User.findOne({ 425 | username 426 | }); 427 | }) 428 | .then(user => { 429 | expect(user).to.not.be.null; 430 | expect(user.firstName).to.equal(firstName); 431 | expect(user.lastName).to.equal(lastName); 432 | }); 433 | }); 434 | }); 435 | 436 | describe('GET', function () { 437 | it('Should return an empty array initially', function () { 438 | return chai.request(app).get('/api/users').then(res => { 439 | expect(res).to.have.status(200); 440 | expect(res.body).to.be.an('array'); 441 | expect(res.body).to.have.length(0); 442 | }); 443 | }); 444 | it('Should return an array of users', function () { 445 | return User.create( 446 | { 447 | username, 448 | password, 449 | firstName, 450 | lastName 451 | }, 452 | { 453 | username: usernameB, 454 | password: passwordB, 455 | firstName: firstNameB, 456 | lastName: lastNameB 457 | } 458 | ) 459 | .then(() => chai.request(app).get('/api/users')) 460 | .then(res => { 461 | expect(res).to.have.status(200); 462 | expect(res.body).to.be.an('array'); 463 | expect(res.body).to.have.length(2); 464 | expect(res.body[0]).to.deep.equal({ 465 | username, 466 | firstName, 467 | lastName 468 | }); 469 | expect(res.body[1]).to.deep.equal({ 470 | username: usernameB, 471 | firstName: firstNameB, 472 | lastName: lastNameB 473 | }); 474 | }); 475 | }); 476 | }); 477 | }); 478 | }); 479 | -------------------------------------------------------------------------------- /users/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {User} = require('./models'); 3 | const {router} = require('./router'); 4 | 5 | module.exports = {User, router}; 6 | -------------------------------------------------------------------------------- /users/models.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const bcrypt = require('bcryptjs'); 3 | const mongoose = require('mongoose'); 4 | 5 | mongoose.Promise = global.Promise; 6 | 7 | const UserSchema = mongoose.Schema({ 8 | username: { 9 | type: String, 10 | required: true, 11 | unique: true 12 | }, 13 | password: { 14 | type: String, 15 | required: true 16 | }, 17 | firstName: {type: String, default: ''}, 18 | lastName: {type: String, default: ''} 19 | }); 20 | 21 | UserSchema.methods.serialize = function() { 22 | return { 23 | username: this.username || '', 24 | firstName: this.firstName || '', 25 | lastName: this.lastName || '' 26 | }; 27 | }; 28 | 29 | UserSchema.methods.validatePassword = function(password) { 30 | return bcrypt.compare(password, this.password); 31 | }; 32 | 33 | UserSchema.statics.hashPassword = function(password) { 34 | return bcrypt.hash(password, 10); 35 | }; 36 | 37 | const User = mongoose.model('User', UserSchema); 38 | 39 | module.exports = {User}; 40 | -------------------------------------------------------------------------------- /users/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | 5 | const {User} = require('./models'); 6 | 7 | const router = express.Router(); 8 | 9 | const jsonParser = bodyParser.json(); 10 | 11 | // Post to register a new user 12 | router.post('/', jsonParser, (req, res) => { 13 | const requiredFields = ['username', 'password']; 14 | const missingField = requiredFields.find(field => !(field in req.body)); 15 | 16 | if (missingField) { 17 | return res.status(422).json({ 18 | code: 422, 19 | reason: 'ValidationError', 20 | message: 'Missing field', 21 | location: missingField 22 | }); 23 | } 24 | 25 | const stringFields = ['username', 'password', 'firstName', 'lastName']; 26 | const nonStringField = stringFields.find( 27 | field => field in req.body && typeof req.body[field] !== 'string' 28 | ); 29 | 30 | if (nonStringField) { 31 | return res.status(422).json({ 32 | code: 422, 33 | reason: 'ValidationError', 34 | message: 'Incorrect field type: expected string', 35 | location: nonStringField 36 | }); 37 | } 38 | 39 | // If the username and password aren't trimmed we give an error. Users might 40 | // expect that these will work without trimming (i.e. they want the password 41 | // "foobar ", including the space at the end). We need to reject such values 42 | // explicitly so the users know what's happening, rather than silently 43 | // trimming them and expecting the user to understand. 44 | // We'll silently trim the other fields, because they aren't credentials used 45 | // to log in, so it's less of a problem. 46 | const explicityTrimmedFields = ['username', 'password']; 47 | const nonTrimmedField = explicityTrimmedFields.find( 48 | field => req.body[field].trim() !== req.body[field] 49 | ); 50 | 51 | if (nonTrimmedField) { 52 | return res.status(422).json({ 53 | code: 422, 54 | reason: 'ValidationError', 55 | message: 'Cannot start or end with whitespace', 56 | location: nonTrimmedField 57 | }); 58 | } 59 | 60 | const sizedFields = { 61 | username: { 62 | min: 1 63 | }, 64 | password: { 65 | min: 10, 66 | // bcrypt truncates after 72 characters, so let's not give the illusion 67 | // of security by storing extra (unused) info 68 | max: 72 69 | } 70 | }; 71 | const tooSmallField = Object.keys(sizedFields).find( 72 | field => 73 | 'min' in sizedFields[field] && 74 | req.body[field].trim().length < sizedFields[field].min 75 | ); 76 | const tooLargeField = Object.keys(sizedFields).find( 77 | field => 78 | 'max' in sizedFields[field] && 79 | req.body[field].trim().length > sizedFields[field].max 80 | ); 81 | 82 | if (tooSmallField || tooLargeField) { 83 | return res.status(422).json({ 84 | code: 422, 85 | reason: 'ValidationError', 86 | message: tooSmallField 87 | ? `Must be at least ${sizedFields[tooSmallField] 88 | .min} characters long` 89 | : `Must be at most ${sizedFields[tooLargeField] 90 | .max} characters long`, 91 | location: tooSmallField || tooLargeField 92 | }); 93 | } 94 | 95 | let {username, password, firstName = '', lastName = ''} = req.body; 96 | // Username and password come in pre-trimmed, otherwise we throw an error 97 | // before this 98 | firstName = firstName.trim(); 99 | lastName = lastName.trim(); 100 | 101 | return User.find({username}) 102 | .count() 103 | .then(count => { 104 | if (count > 0) { 105 | // There is an existing user with the same username 106 | return Promise.reject({ 107 | code: 422, 108 | reason: 'ValidationError', 109 | message: 'Username already taken', 110 | location: 'username' 111 | }); 112 | } 113 | // If there is no existing user, hash the password 114 | return User.hashPassword(password); 115 | }) 116 | .then(hash => { 117 | return User.create({ 118 | username, 119 | password: hash, 120 | firstName, 121 | lastName 122 | }); 123 | }) 124 | .then(user => { 125 | return res.status(201).json(user.serialize()); 126 | }) 127 | .catch(err => { 128 | // Forward validation errors on to the client, otherwise give a 500 129 | // error because something unexpected has happened 130 | if (err.reason === 'ValidationError') { 131 | return res.status(err.code).json(err); 132 | } 133 | res.status(500).json({code: 500, message: 'Internal server error'}); 134 | }); 135 | }); 136 | 137 | // Never expose all your users like below in a prod application 138 | // we're just doing this so we have a quick way to see 139 | // if we're creating users. keep in mind, you can also 140 | // verify this in the Mongo shell. 141 | router.get('/', (req, res) => { 142 | return User.find() 143 | .then(users => res.json(users.map(user => user.serialize()))) 144 | .catch(err => res.status(500).json({message: 'Internal server error'})); 145 | }); 146 | 147 | module.exports = {router}; 148 | --------------------------------------------------------------------------------