[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\n    \"es2015-node5\",\n    \"stage-0\"\n  ]\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\n\n# A special property that should be specified at the top of the file outside of\n# any sections. Set to true to stop .editor config file search on current file\nroot = true\n\n[*]\n# Indentation style\n# Possible values - tab, space\nindent_style = space\n\n# Indentation size in single-spaced characters\n# Possible values - an integer, tab\nindent_size = 2\n\n# Line ending file format\n# Possible values - lf, crlf, cr\nend_of_line = lf\n\n# File character encoding\n# Possible values - latin1, utf-8, utf-16be, utf-16le\ncharset = utf-8\n\n# Denotes whether to trim whitespace at the end of lines\n# Possible values - true, false\ntrim_trailing_whitespace = true\n\n# Denotes whether file should end with a newline\n# Possible values - true, false\ninsert_final_newline = true\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"extends\": \"standard\",\n  \"env\": {\n    \"node\": true,\n    \"mocha\": true\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "\n\n# Created by https://www.gitignore.io/api/node,sublimetext\n\n### Node ###\n# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directory\nnode_modules\n\n# Optional npm cache directory\n.npm\n\n# Optional REPL history\n.node_repl_history\n\n\n### SublimeText ###\n# cache files for sublime text\n*.tmlanguage.cache\n*.tmPreferences.cache\n*.stTheme.cache\n\n# workspace files are user-specific\n*.sublime-workspace\n\n# project files should be checked into the repository, unless a significant\n# proportion of contributors will probably not be using SublimeText\n*.sublime-project\n\n# sftp configuration file\nsftp-config.json\n\n#Documentation\ndocs\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\nCopyright (c) 2016 Adrian Obelmejias <adrian@obel.me>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "#koa2-api-boilerplate\n[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com)\n\nBoilerplate for building APIs with [koa2](https://github.com/koajs/koa/tree/v2.x) and mongodb.\n\nThis project covers basic necessities of most APIs.\n* Authentication (passport & jwt)\n* Database (mongoose)\n* Testing (mocha)\n* Doc generation with apidoc\n* linting using standard\n\n##Requirements\n* node __^4.0.0__\n* npm __^3.0.0__\n\n##Installation\n```bash\ngit clone https://github.com/adrianObel/koa2-api-boilerplate.git\n```\n\n##Features\n* [koa2](https://github.com/koajs/koa/tree/v2.x)\n* [koa-router](https://github.com/alexmingoia/koa-router)\n* [koa-bodyparser](https://github.com/koajs/bodyparser)\n* [koa-generic-session](https://github.com/koajs/generic-session)\n* [koa-logger](https://github.com/koajs/logger)\n* [MongoDB](http://mongodb.org/)\n* [Mongoose](http://mongoosejs.com/)\n* [Passport](http://passportjs.org/)\n* [Nodemon](http://nodemon.io/)\n* [Mocha](https://mochajs.org/)\n* [apidoc](http://apidocjs.com/)\n* [Babel](https://github.com/babel/babel)\n* [ESLint](http://eslint.org/)\n\n##Structure\n```\n├── bin\n│   └── server.js            # Bootstrapping and entry point\n├── config                   # Server configuration settings\n│   ├── env                  # Environment specific config\n│   │   ├── common.js\n│   │   ├── development.js\n│   │   ├── production.js\n│   │   └── test.js\n│   ├── index.js             # Config entrypoint - exports config according to envionrment and commons\n│   └── passport.js          # Passportjs config of strategies\n├── src                      # Source code\n│   ├── modules\n│   │   ├── controller.js    # Module-specific controllers\n│   │   └── router.js        # Router definitions for module\n│   ├── models               # Mongoose models\n│   └── middleware           # Custom middleware\n│       └── validators       # Validation middleware\n└── test                     # Unit tests\n```\n\n##Usage\n* `npm start` Start server on live mode\n* `npm run dev` Start server on dev mode with nodemon\n* `npm run docs` Generate API documentation\n* `npm test` Run mocha tests\n\n##Documentation\nAPI documentation is written inline and generated by [apidoc](http://apidocjs.com/).\n\nVisit `http://localhost:5000/docs/` to view docs\n\n##License\nMIT\n"
  },
  {
    "path": "bin/server.js",
    "content": "import Koa from 'koa'\nimport bodyParser from 'koa-bodyparser'\nimport convert from 'koa-convert'\nimport logger from 'koa-logger'\nimport mongoose from 'mongoose'\nimport session from 'koa-generic-session'\nimport passport from 'koa-passport'\nimport mount from 'koa-mount'\nimport serve from 'koa-static'\n\nimport config from '../config'\nimport { errorMiddleware } from '../src/middleware'\n\nconst app = new Koa()\napp.keys = [config.session]\n\nmongoose.Promise = global.Promise\nmongoose.connect(config.database)\n\napp.use(convert(logger()))\napp.use(bodyParser())\napp.use(session())\napp.use(errorMiddleware())\n\napp.use(convert(mount('/docs', serve(`${process.cwd()}/docs`))))\n\nrequire('../config/passport')\napp.use(passport.initialize())\napp.use(passport.session())\n\nconst modules = require('../src/modules')\nmodules(app)\n\napp.listen(config.port, () => {\n  console.log(`Server started on ${config.port}`)\n})\n\nexport default app\n"
  },
  {
    "path": "config/env/common.js",
    "content": "export default {\n  port: process.env.PORT || 5000\n}\n"
  },
  {
    "path": "config/env/development.js",
    "content": "export default {\n  session: 'secret-boilerplate-token',\n  token: 'secret-jwt-token',\n  database: 'mongodb://localhost:27017/koa2-boilerplate-dev'\n}\n"
  },
  {
    "path": "config/env/production.js",
    "content": "export default {\n  session: 'secret-boilerplate-token',\n  token: 'secret-jwt-token',\n  database: 'mongodb://localhost:27017/koa2-boilerplate-prod'\n}\n"
  },
  {
    "path": "config/env/test.js",
    "content": "export default {\n  session: 'secret-boilerplate-token',\n  token: 'secret-jwt-token',\n  database: 'mongodb://localhost:27017/koa2-boilerplate-test'\n}\n"
  },
  {
    "path": "config/index.js",
    "content": "import common from './env/common'\n\nconst env = process.env.NODE_ENV || 'development'\nconst config = require(`./env/${env}`).default\n\nexport default Object.assign({}, common, config)\n"
  },
  {
    "path": "config/passport.js",
    "content": "import passport from 'koa-passport'\nimport User from '../src/models/users'\nimport { Strategy } from 'passport-local'\n\npassport.serializeUser((user, done) => {\n  done(null, user.id)\n})\n\npassport.deserializeUser(async (id, done) => {\n  try {\n    const user = await User.findById(id, '-password')\n    done(null, user)\n  } catch (err) {\n    done(err)\n  }\n})\n\npassport.use('local', new Strategy({\n  usernameField: 'username',\n  passwordField: 'password'\n}, async (username, password, done) => {\n  try {\n    const user = await User.findOne({ username })\n    if (!user) { return done(null, false) }\n\n    try {\n      const isMatch = await user.validatePassword(password)\n\n      if (!isMatch) { return done(null, false) }\n\n      done(null, user)\n    } catch (err) {\n      done(err)\n    }\n\n  } catch (err) {\n    return done(err)\n  }\n}))\n"
  },
  {
    "path": "index.js",
    "content": "require('babel-core/register')()\nrequire('babel-polyfill')\nrequire('./bin/server.js')\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"koa2-api-boilerplate\",\n  \"version\": \"2.2.0\",\n  \"description\": \"Koa2 boilerplate covering essentials for APIs\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"node index.js\",\n    \"dev\": \"./node_modules/.bin/nodemon index.js\",\n    \"test\": \"NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-register --require babel-polyfill\",\n    \"lint\": \"eslint src/**/*.js\",\n    \"docs\": \"./node_modules/.bin/apidoc -i src/ -o docs\"\n  },\n  \"keywords\": [\n    \"koa2-api-boilerplate\",\n    \"api\",\n    \"koa\",\n    \"koa2\",\n    \"boilerplate\",\n    \"es6\",\n    \"mongoose\",\n    \"passportjs\",\n    \"apidoc\"\n  ],\n  \"author\": \"Adrian Obelmejias <adrian@obel.me>\",\n  \"license\": \"MIT\",\n  \"apidoc\": {\n    \"title\": \"koa2-api-boilerplate\",\n    \"url\": \"localhost:5000\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/adrianObel/koa2-api-boilerplate\"\n  },\n  \"dependencies\": {\n    \"apidoc\": \"^0.16.1\",\n    \"babel-core\": \"^6.5.1\",\n    \"babel-polyfill\": \"^6.5.0\",\n    \"babel-preset-es2015-node5\": \"^1.2.0\",\n    \"babel-preset-stage-0\": \"^6.5.0\",\n    \"bcrypt\": \"^0.8.5\",\n    \"glob\": \"^7.0.0\",\n    \"jsonwebtoken\": \"^7.1.9\",\n    \"koa\": \"^2.0.0-alpha.6\",\n    \"koa-bodyparser\": \"^3.0.0\",\n    \"koa-convert\": \"^1.2.0\",\n    \"koa-generic-session\": \"^1.10.1\",\n    \"koa-logger\": \"^2.0.0\",\n    \"koa-mount\": \"^1.3.0\",\n    \"koa-passport\": \"^2.0.1\",\n    \"koa-router\": \"^7.0.1\",\n    \"koa-static\": \"^2.0.0\",\n    \"mongoose\": \"^4.4.3\",\n    \"passport-local\": \"^1.0.0\"\n  },\n  \"devDependencies\": {\n    \"babel-eslint\": \"^6.0.2\",\n    \"babel-register\": \"^6.5.1\",\n    \"chai\": \"^3.5.0\",\n    \"eslint\": \"^3.4.0\",\n    \"eslint-config-standard\": \"^6.0.0\",\n    \"eslint-plugin-promise\": \"^2.0.1\",\n    \"eslint-plugin-standard\": \"^2.0.0\",\n    \"mocha\": \"^3.0.2\",\n    \"nodemon\": \"^1.8.1\",\n    \"supertest\": \"^2.0.0\"\n  }\n}\n"
  },
  {
    "path": "src/middleware/index.js",
    "content": "export function errorMiddleware () {\n  return async (ctx, next) => {\n    try {\n      await next()\n    } catch (err) {\n      ctx.status = err.status || 500\n      ctx.body = err.message\n      ctx.app.emit('error', err, ctx)\n    }\n  }\n}\n"
  },
  {
    "path": "src/middleware/validators.js",
    "content": "import User from '../models/users'\nimport config from '../../config'\nimport { getToken } from '../utils/auth'\nimport { verify } from 'jsonwebtoken'\n\nexport async function ensureUser (ctx, next) {\n  const token = getToken(ctx)\n\n  if (!token) {\n    ctx.throw(401)\n  }\n\n  let decoded = null\n  try {\n    decoded = verify(token, config.token)\n  } catch (err) {\n    ctx.throw(401)\n  }\n\n  ctx.state.user = await User.findById(decoded.id, '-password')\n  if (!ctx.state.user) {\n    ctx.throw(401)\n  }\n\n  return next()\n}\n"
  },
  {
    "path": "src/models/users.js",
    "content": "import mongoose from 'mongoose'\nimport bcrypt from 'bcrypt'\nimport config from '../../config'\nimport jwt from 'jsonwebtoken'\n\nconst User = new mongoose.Schema({\n  type: { type: String, default: 'User' },\n  name: { type: String },\n  username: { type: String, required: true, unique: true },\n  password: { type: String, required: true }\n})\n\nUser.pre('save', function preSave (next) {\n  const user = this\n\n  if (!user.isModified('password')) {\n    return next()\n  }\n\n  new Promise((resolve, reject) => {\n    bcrypt.genSalt(10, (err, salt) => {\n      if (err) { return reject(err) }\n      resolve(salt)\n    })\n  })\n  .then(salt => {\n    bcrypt.hash(user.password, salt, (err, hash) => {\n      if (err) { throw new Error(err) }\n\n      user.password = hash\n\n      next(null)\n    })\n  })\n  .catch(err => next(err))\n})\n\nUser.methods.validatePassword = function validatePassword (password) {\n  const user = this\n\n  return new Promise((resolve, reject) => {\n    bcrypt.compare(password, user.password, (err, isMatch) => {\n      if (err) { return reject(err) }\n\n      resolve(isMatch)\n    })\n  })\n}\n\nUser.methods.generateToken = function generateToken () {\n  const user = this\n\n  return jwt.sign({ id: user.id }, config.token)\n}\n\nexport default mongoose.model('user', User)\n"
  },
  {
    "path": "src/modules/auth/controller.js",
    "content": "import passport from 'koa-passport'\n\n/**\n * @apiDefine TokenError\n * @apiError Unauthorized Invalid JWT token\n *\n * @apiErrorExample {json} Unauthorized-Error:\n *     HTTP/1.1 401 Unauthorized\n *     {\n *       \"status\": 401,\n *       \"error\": \"Unauthorized\"\n *     }\n */\n\n/**\n * @api {post} /auth Authenticate user\n * @apiVersion 1.0.0\n * @apiName AuthUser\n * @apiGroup Auth\n *\n * @apiParam {String} username  User username.\n * @apiParam {String} password  User password.\n *\n * @apiExample Example usage:\n * curl -H \"Content-Type: application/json\" -X POST -d '{ \"username\": \"johndoe@gmail.com\", \"password\": \"foo\" }' localhost:5000/auth\n *\n * @apiSuccess {Object}   user           User object\n * @apiSuccess {ObjectId} user._id       User id\n * @apiSuccess {String}   user.name      User name\n * @apiSuccess {String}   user.username  User username\n * @apiSuccess {String}   token          Encoded JWT\n *\n * @apiSuccessExample {json} Success-Response:\n *     HTTP/1.1 200 OK\n *     {\n *       \"user\": {\n *          \"_id\": \"56bd1da600a526986cf65c80\"\n *          \"username\": \"johndoe\"\n *        },\n *       \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ\"\n *     }\n *\n * @apiError Unauthorized Incorrect credentials\n *\n * @apiErrorExample {json} Error-Response:\n *     HTTP/1.1 401 Unauthorized\n *     {\n *       \"status\": 401,\n *       \"error\": \"Unauthorized\"\n *     }\n */\n\nexport async function authUser (ctx, next) {\n  return passport.authenticate('local', (user) => {\n    if (!user) {\n      ctx.throw(401)\n    }\n\n    const token = user.generateToken()\n\n    const response = user.toJSON()\n\n    delete response.password\n\n    ctx.body = {\n      token,\n      user: response\n    }\n  })(ctx, next)\n}\n"
  },
  {
    "path": "src/modules/auth/router.js",
    "content": "import * as auth from './controller'\n\nexport const baseUrl = '/auth'\n\nexport default [\n  {\n    method: 'POST',\n    route: '/',\n    handlers: [\n      auth.authUser\n    ]\n  }\n]\n"
  },
  {
    "path": "src/modules/index.js",
    "content": "import glob from 'glob'\nimport Router from 'koa-router'\n\nexports = module.exports = function initModules (app) {\n  glob(`${__dirname}/*`, { ignore: '**/index.js' }, (err, matches) => {\n    if (err) { throw err }\n\n    matches.forEach((mod) => {\n      const router = require(`${mod}/router`)\n\n      const routes = router.default\n      const baseUrl = router.baseUrl\n      const instance = new Router({ prefix: baseUrl })\n\n      routes.forEach((config) => {\n        const {\n          method = '',\n          route = '',\n          handlers = []\n        } = config\n\n        const lastHandler = handlers.pop()\n\n        instance[method.toLowerCase()](route, ...handlers, async function(ctx) {\n          return await lastHandler(ctx)\n        })\n\n        app\n          .use(instance.routes())\n          .use(instance.allowedMethods())\n      })\n    })\n  })\n}\n"
  },
  {
    "path": "src/modules/users/controller.js",
    "content": "import User from '../../models/users'\n\n/**\n * @api {post} /users Create a new user\n * @apiPermission\n * @apiVersion 1.0.0\n * @apiName CreateUser\n * @apiGroup Users\n *\n * @apiExample Example usage:\n * curl -H \"Content-Type: application/json\" -X POST -d '{ \"user\": { \"username\": \"johndoe\", \"password\": \"secretpasas\" } }' localhost:5000/users\n *\n * @apiParam {Object} user          User object (required)\n * @apiParam {String} user.username Username.\n * @apiParam {String} user.password Password.\n *\n * @apiSuccess {Object}   users           User object\n * @apiSuccess {ObjectId} users._id       User id\n * @apiSuccess {String}   users.name      User name\n * @apiSuccess {String}   users.username  User username\n *\n * @apiSuccessExample {json} Success-Response:\n *     HTTP/1.1 200 OK\n *     {\n *       \"user\": {\n *          \"_id\": \"56bd1da600a526986cf65c80\"\n *          \"name\": \"John Doe\"\n *          \"username\": \"johndoe\"\n *       }\n *     }\n *\n * @apiError UnprocessableEntity Missing required parameters\n *\n * @apiErrorExample {json} Error-Response:\n *     HTTP/1.1 422 Unprocessable Entity\n *     {\n *       \"status\": 422,\n *       \"error\": \"Unprocessable Entity\"\n *     }\n */\nexport async function createUser (ctx) {\n  const user = new User(ctx.request.body.user)\n  try {\n    await user.save()\n  } catch (err) {\n    ctx.throw(422, err.message)\n  }\n\n  const token = user.generateToken()\n  const response = user.toJSON()\n\n  delete response.password\n\n  ctx.body = {\n    user: response,\n    token\n  }\n}\n\n/**\n * @api {get} /users Get all users\n * @apiPermission user\n * @apiVersion 1.0.0\n * @apiName GetUsers\n * @apiGroup Users\n *\n * @apiExample Example usage:\n * curl -H \"Content-Type: application/json\" -X GET localhost:5000/users\n *\n * @apiSuccess {Object[]} users           Array of user objects\n * @apiSuccess {ObjectId} users._id       User id\n * @apiSuccess {String}   users.name      User name\n * @apiSuccess {String}   users.username  User username\n *\n * @apiSuccessExample {json} Success-Response:\n *     HTTP/1.1 200 OK\n *     {\n *       \"users\": [{\n *          \"_id\": \"56bd1da600a526986cf65c80\"\n *          \"name\": \"John Doe\"\n *          \"username\": \"johndoe\"\n *       }]\n *     }\n *\n * @apiUse TokenError\n */\nexport async function getUsers (ctx) {\n  const users = await User.find({}, '-password')\n  ctx.body = { users }\n}\n\n/**\n * @api {get} /users/:id Get user by id\n * @apiPermission user\n * @apiVersion 1.0.0\n * @apiName GetUser\n * @apiGroup Users\n *\n * @apiExample Example usage:\n * curl -H \"Content-Type: application/json\" -X GET localhost:5000/users/56bd1da600a526986cf65c80\n *\n * @apiSuccess {Object}   users           User object\n * @apiSuccess {ObjectId} users._id       User id\n * @apiSuccess {String}   users.name      User name\n * @apiSuccess {String}   users.username  User username\n *\n * @apiSuccessExample {json} Success-Response:\n *     HTTP/1.1 200 OK\n *     {\n *       \"user\": {\n *          \"_id\": \"56bd1da600a526986cf65c80\"\n *          \"name\": \"John Doe\"\n *          \"username\": \"johndoe\"\n *       }\n *     }\n *\n * @apiUse TokenError\n */\nexport async function getUser (ctx, next) {\n  try {\n    const user = await User.findById(ctx.params.id, '-password')\n    if (!user) {\n      ctx.throw(404)\n    }\n\n    ctx.body = {\n      user\n    }\n  } catch (err) {\n    if (err === 404 || err.name === 'CastError') {\n      ctx.throw(404)\n    }\n\n    ctx.throw(500)\n  }\n\n  if (next) { return next() }\n}\n\n/**\n * @api {put} /users/:id Update a user\n * @apiPermission\n * @apiVersion 1.0.0\n * @apiName UpdateUser\n * @apiGroup Users\n *\n * @apiExample Example usage:\n * curl -H \"Content-Type: application/json\" -X PUT -d '{ \"user\": { \"name\": \"Cool new Name\" } }' localhost:5000/users/56bd1da600a526986cf65c80\n *\n * @apiParam {Object} user          User object (required)\n * @apiParam {String} user.name     Name.\n * @apiParam {String} user.username Username.\n *\n * @apiSuccess {Object}   users           User object\n * @apiSuccess {ObjectId} users._id       User id\n * @apiSuccess {String}   users.name      Updated name\n * @apiSuccess {String}   users.username  Updated username\n *\n * @apiSuccessExample {json} Success-Response:\n *     HTTP/1.1 200 OK\n *     {\n *       \"user\": {\n *          \"_id\": \"56bd1da600a526986cf65c80\"\n *          \"name\": \"Cool new name\"\n *          \"username\": \"johndoe\"\n *       }\n *     }\n *\n * @apiError UnprocessableEntity Missing required parameters\n *\n * @apiErrorExample {json} Error-Response:\n *     HTTP/1.1 422 Unprocessable Entity\n *     {\n *       \"status\": 422,\n *       \"error\": \"Unprocessable Entity\"\n *     }\n *\n * @apiUse TokenError\n */\nexport async function updateUser (ctx) {\n  const user = ctx.body.user\n\n  Object.assign(user, ctx.request.body.user)\n\n  await user.save()\n\n  ctx.body = {\n    user\n  }\n}\n\n/**\n * @api {delete} /users/:id Delete a user\n * @apiPermission\n * @apiVersion 1.0.0\n * @apiName DeleteUser\n * @apiGroup Users\n *\n * @apiExample Example usage:\n * curl -H \"Content-Type: application/json\" -X DELETE localhost:5000/users/56bd1da600a526986cf65c80\n *\n * @apiSuccess {StatusCode} 200\n *\n * @apiSuccessExample {json} Success-Response:\n *     HTTP/1.1 200 OK\n *     {\n *       \"success\": true\n *     }\n *\n * @apiUse TokenError\n */\n\nexport async function deleteUser (ctx) {\n  const user = ctx.body.user\n\n  await user.remove()\n\n  ctx.status = 200\n  ctx.body = {\n    success: true\n  }\n}\n"
  },
  {
    "path": "src/modules/users/router.js",
    "content": "import { ensureUser } from '../../middleware/validators'\nimport * as user from './controller'\n\nexport const baseUrl = '/users'\n\nexport default [\n  {\n    method: 'POST',\n    route: '/',\n    handlers: [\n      user.createUser\n    ]\n  },\n  {\n    method: 'GET',\n    route: '/',\n    handlers: [\n      ensureUser,\n      user.getUsers\n    ]\n  },\n  {\n    method: 'GET',\n    route: '/:id',\n    handlers: [\n      ensureUser,\n      user.getUser\n    ]\n  },\n  {\n    method: 'PUT',\n    route: '/:id',\n    handlers: [\n      ensureUser,\n      user.getUser,\n      user.updateUser\n    ]\n  },\n  {\n    method: 'DELETE',\n    route: '/:id',\n    handlers: [\n      ensureUser,\n      user.getUser,\n      user.deleteUser\n    ]\n  }\n]\n"
  },
  {
    "path": "src/utils/auth.js",
    "content": "export function getToken (ctx) {\n  const header = ctx.request.header.authorization\n  if (!header) {\n    return null\n  }\n  const parts = header.split(' ')\n  if (parts.length !== 2) {\n    return null\n  }\n  const scheme = parts[0]\n  const token = parts[1]\n  if (/^Bearer$/i.test(scheme)) {\n    return token\n  }\n  return null\n}\n"
  },
  {
    "path": "test/auth.spec.js",
    "content": "import app from '../bin/server'\nimport supertest from 'supertest'\nimport { expect, should } from 'chai'\nimport { cleanDb, authUser } from './utils'\n\nshould()\nconst request = supertest.agent(app.listen())\nconst context = {}\n\ndescribe('Auth', () => {\n  before((done) => {\n    cleanDb()\n    authUser(request, (err, { user, token }) => {\n      if (err) { return done(err) }\n\n      context.user = user\n      context.token = token\n      done()\n    })\n  })\n\n  describe('POST /auth', () => {\n    it('should throw 401 if credentials are incorrect', (done) => {\n      request\n        .post('/auth')\n        .set('Accept', 'application/json')\n        .send({ username: 'supercoolname', password: 'wrongpassword' })\n        .expect(401, done)\n    })\n\n    it('should auth user', (done) => {\n      request\n        .post('/auth')\n        .set('Accept', 'application/json')\n        .send({ username: 'test', password: 'pass' })\n        .expect(200, (err, res) => {\n          if (err) { return done(err) }\n\n          res.body.user.should.have.property('username')\n          res.body.user.username.should.equal('test')\n          expect(res.body.user.password).to.not.exist\n\n          context.user = res.body.user\n          context.token = res.body.token\n\n          done()\n        })\n    })\n  })\n})\n"
  },
  {
    "path": "test/users.spec.js",
    "content": "import app from '../bin/server'\nimport supertest from 'supertest'\nimport { expect, should } from 'chai'\nimport { cleanDb } from './utils'\n\nshould()\nconst request = supertest.agent(app.listen())\nconst context = {}\n\ndescribe('Users', () => {\n  before((done) => {\n    cleanDb()\n    done()\n  })\n\n  describe('POST /users', () => {\n    it('should reject signup when data is incomplete', (done) => {\n      request\n        .post('/users')\n        .set('Accept', 'application/json')\n        .send({ username: 'supercoolname' })\n        .expect(422, done)\n    })\n\n    it('should sign up', (done) => {\n      request\n        .post('/users')\n        .set('Accept', 'application/json')\n        .send({ user: { username: 'supercoolname', password: 'supersecretpassword' } })\n        .expect(200, (err, res) => {\n          if (err) { return done(err) }\n\n          res.body.user.should.have.property('username')\n          res.body.user.username.should.equal('supercoolname')\n          expect(res.body.user.password).to.not.exist\n\n          context.user = res.body.user\n          context.token = res.body.token\n\n          done()\n        })\n    })\n  })\n\n  describe('GET /users', () => {\n    it('should not fetch users if the authorization header is missing', (done) => {\n      request\n        .get('/users')\n        .set('Accept', 'application/json')\n        .expect(401, done)\n    })\n\n    it('should not fetch users if the authorization header is missing the scheme', (done) => {\n      request\n        .get('/users')\n        .set({\n          Accept: 'application/json',\n          Authorization: '1'\n        })\n        .expect(401, done)\n    })\n\n    it('should not fetch users if the authorization header has invalid scheme', (done) => {\n      const { token } = context\n      request\n        .get('/users')\n        .set({\n          Accept: 'application/json',\n          Authorization: `Unknown ${token}`\n        })\n        .expect(401, done)\n    })\n\n    it('should not fetch users if token is invalid', (done) => {\n      request\n        .get('/users')\n        .set({\n          Accept: 'application/json',\n          Authorization: 'Bearer 1'\n        })\n        .expect(401, done)\n    })\n\n    it('should fetch all users', (done) => {\n      const { token } = context\n      request\n        .get('/users')\n        .set({\n          Accept: 'application/json',\n          Authorization: `Bearer ${token}`\n        })\n        .expect(200, (err, res) => {\n          if (err) { return done(err) }\n\n          res.body.should.have.property('users')\n\n          res.body.users.should.have.length(1)\n\n          done()\n        })\n    })\n  })\n\n  describe('GET /users/:id', () => {\n    it('should not fetch user if token is invalid', (done) => {\n      request\n        .get('/users/1')\n        .set({\n          Accept: 'application/json',\n          Authorization: 'Bearer 1'\n        })\n        .expect(401, done)\n    })\n\n    it('should throw 404 if user doesn\\'t exist', (done) => {\n      const { token } = context\n      request\n        .get('/users/1')\n        .set({\n          Accept: 'application/json',\n          Authorization: `Bearer ${token}`\n        })\n        .expect(404, done)\n    })\n\n    it('should fetch user', (done) => {\n      const {\n        user: { _id },\n        token\n      } = context\n\n      request\n        .get(`/users/${_id}`)\n        .set({\n          Accept: 'application/json',\n          Authorization: `Bearer ${token}`\n        })\n        .expect(200, (err, res) => {\n          if (err) { return done(err) }\n\n          res.body.should.have.property('user')\n\n          expect(res.body.user.password).to.not.exist\n\n          done()\n        })\n    })\n  })\n\n  describe('PUT /users/:id', () => {\n    it('should not update user if token is invalid', (done) => {\n      request\n        .put('/users/1')\n        .set({\n          Accept: 'application/json',\n          Authorization: 'Bearer 1'\n        })\n        .expect(401, done)\n    })\n\n    it('should throw 404 if user doesn\\'t exist', (done) => {\n      const { token } = context\n      request\n        .put('/users/1')\n        .set({\n          Accept: 'application/json',\n          Authorization: `Bearer ${token}`\n        })\n        .expect(404, done)\n    })\n\n    it('should update user', (done) => {\n      const {\n        user: { _id },\n        token\n      } = context\n\n      request\n        .put(`/users/${_id}`)\n        .set({\n          Accept: 'application/json',\n          Authorization: `Bearer ${token}`\n        })\n        .send({ user: { username: 'updatedcoolname' } })\n        .expect(200, (err, res) => {\n          if (err) { return done(err) }\n\n          res.body.user.should.have.property('username')\n          res.body.user.username.should.equal('updatedcoolname')\n          expect(res.body.user.password).to.not.exist\n\n          done()\n        })\n    })\n  })\n\n  describe('DELETE /users/:id', () => {\n    it('should not delete user if token is invalid', (done) => {\n      request\n        .delete('/users/1')\n        .set({\n          Accept: 'application/json',\n          Authorization: 'Bearer 1'\n        })\n        .expect(401, done)\n    })\n\n    it('should throw 404 if user doesn\\'t exist', (done) => {\n      const { token } = context\n      request\n        .delete('/users/1')\n        .set({\n          Accept: 'application/json',\n          Authorization: `Bearer ${token}`\n        })\n        .expect(404, done)\n    })\n\n    it('should delete user', (done) => {\n      const {\n        user: { _id },\n        token\n      } = context\n\n      request\n        .delete(`/users/${_id}`)\n        .set({\n          Accept: 'application/json',\n          Authorization: `Bearer ${token}`\n        })\n        .expect(200, done)\n    })\n  })\n})\n"
  },
  {
    "path": "test/utils.js",
    "content": "import mongoose from 'mongoose'\n\nexport function cleanDb () {\n  for (const collection in mongoose.connection.collections) {\n    if (mongoose.connection.collections.hasOwnProperty(collection)) {\n      mongoose.connection.collections[collection].remove()\n    }\n  }\n}\n\nexport function authUser (agent, callback) {\n  agent\n    .post('/users')\n    .set('Accept', 'application/json')\n    .send({ user: { username: 'test', password: 'pass' } })\n    .end((err, res) => {\n      if (err) { return callback(err) }\n\n      callback(null, {\n        user: res.body.user,\n        token: res.body.token\n      })\n    })\n}\n"
  }
]