master 7c9c26b7f9d6 cached
27 files
27.8 KB
8.3k tokens
11 symbols
1 requests
Download .txt
Repository: adrianObel/koa2-api-boilerplate
Branch: master
Commit: 7c9c26b7f9d6
Files: 27
Total size: 27.8 KB

Directory structure:
gitextract_sjp2bf63/

├── .babelrc
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── bin/
│   └── server.js
├── config/
│   ├── env/
│   │   ├── common.js
│   │   ├── development.js
│   │   ├── production.js
│   │   └── test.js
│   ├── index.js
│   └── passport.js
├── index.js
├── package.json
├── src/
│   ├── middleware/
│   │   ├── index.js
│   │   └── validators.js
│   ├── models/
│   │   └── users.js
│   ├── modules/
│   │   ├── auth/
│   │   │   ├── controller.js
│   │   │   └── router.js
│   │   ├── index.js
│   │   └── users/
│   │       ├── controller.js
│   │       └── router.js
│   └── utils/
│       └── auth.js
└── test/
    ├── auth.spec.js
    ├── users.spec.js
    └── utils.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
  "presets": [
    "es2015-node5",
    "stage-0"
  ]
}


================================================
FILE: .editorconfig
================================================
# http://editorconfig.org

# A special property that should be specified at the top of the file outside of
# any sections. Set to true to stop .editor config file search on current file
root = true

[*]
# Indentation style
# Possible values - tab, space
indent_style = space

# Indentation size in single-spaced characters
# Possible values - an integer, tab
indent_size = 2

# Line ending file format
# Possible values - lf, crlf, cr
end_of_line = lf

# File character encoding
# Possible values - latin1, utf-8, utf-16be, utf-16le
charset = utf-8

# Denotes whether to trim whitespace at the end of lines
# Possible values - true, false
trim_trailing_whitespace = true

# Denotes whether file should end with a newline
# Possible values - true, false
insert_final_newline = true


================================================
FILE: .eslintrc.json
================================================
{
  "parser": "babel-eslint",
  "extends": "standard",
  "env": {
    "node": true,
    "mocha": true
  }
}


================================================
FILE: .gitignore
================================================


# Created by https://www.gitignore.io/api/node,sublimetext

### Node ###
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directory
node_modules

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history


### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache

# workspace files are user-specific
*.sublime-workspace

# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
*.sublime-project

# sftp configuration file
sftp-config.json

#Documentation
docs


================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016 Adrian Obelmejias <adrian@obel.me>

Permission 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:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE 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.

================================================
FILE: README.md
================================================
#koa2-api-boilerplate
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com)

Boilerplate for building APIs with [koa2](https://github.com/koajs/koa/tree/v2.x) and mongodb.

This project covers basic necessities of most APIs.
* Authentication (passport & jwt)
* Database (mongoose)
* Testing (mocha)
* Doc generation with apidoc
* linting using standard

##Requirements
* node __^4.0.0__
* npm __^3.0.0__

##Installation
```bash
git clone https://github.com/adrianObel/koa2-api-boilerplate.git
```

##Features
* [koa2](https://github.com/koajs/koa/tree/v2.x)
* [koa-router](https://github.com/alexmingoia/koa-router)
* [koa-bodyparser](https://github.com/koajs/bodyparser)
* [koa-generic-session](https://github.com/koajs/generic-session)
* [koa-logger](https://github.com/koajs/logger)
* [MongoDB](http://mongodb.org/)
* [Mongoose](http://mongoosejs.com/)
* [Passport](http://passportjs.org/)
* [Nodemon](http://nodemon.io/)
* [Mocha](https://mochajs.org/)
* [apidoc](http://apidocjs.com/)
* [Babel](https://github.com/babel/babel)
* [ESLint](http://eslint.org/)

##Structure
```
├── bin
│   └── server.js            # Bootstrapping and entry point
├── config                   # Server configuration settings
│   ├── env                  # Environment specific config
│   │   ├── common.js
│   │   ├── development.js
│   │   ├── production.js
│   │   └── test.js
│   ├── index.js             # Config entrypoint - exports config according to envionrment and commons
│   └── passport.js          # Passportjs config of strategies
├── src                      # Source code
│   ├── modules
│   │   ├── controller.js    # Module-specific controllers
│   │   └── router.js        # Router definitions for module
│   ├── models               # Mongoose models
│   └── middleware           # Custom middleware
│       └── validators       # Validation middleware
└── test                     # Unit tests
```

##Usage
* `npm start` Start server on live mode
* `npm run dev` Start server on dev mode with nodemon
* `npm run docs` Generate API documentation
* `npm test` Run mocha tests

##Documentation
API documentation is written inline and generated by [apidoc](http://apidocjs.com/).

Visit `http://localhost:5000/docs/` to view docs

##License
MIT


================================================
FILE: bin/server.js
================================================
import Koa from 'koa'
import bodyParser from 'koa-bodyparser'
import convert from 'koa-convert'
import logger from 'koa-logger'
import mongoose from 'mongoose'
import session from 'koa-generic-session'
import passport from 'koa-passport'
import mount from 'koa-mount'
import serve from 'koa-static'

import config from '../config'
import { errorMiddleware } from '../src/middleware'

const app = new Koa()
app.keys = [config.session]

mongoose.Promise = global.Promise
mongoose.connect(config.database)

app.use(convert(logger()))
app.use(bodyParser())
app.use(session())
app.use(errorMiddleware())

app.use(convert(mount('/docs', serve(`${process.cwd()}/docs`))))

require('../config/passport')
app.use(passport.initialize())
app.use(passport.session())

const modules = require('../src/modules')
modules(app)

app.listen(config.port, () => {
  console.log(`Server started on ${config.port}`)
})

export default app


================================================
FILE: config/env/common.js
================================================
export default {
  port: process.env.PORT || 5000
}


================================================
FILE: config/env/development.js
================================================
export default {
  session: 'secret-boilerplate-token',
  token: 'secret-jwt-token',
  database: 'mongodb://localhost:27017/koa2-boilerplate-dev'
}


================================================
FILE: config/env/production.js
================================================
export default {
  session: 'secret-boilerplate-token',
  token: 'secret-jwt-token',
  database: 'mongodb://localhost:27017/koa2-boilerplate-prod'
}


================================================
FILE: config/env/test.js
================================================
export default {
  session: 'secret-boilerplate-token',
  token: 'secret-jwt-token',
  database: 'mongodb://localhost:27017/koa2-boilerplate-test'
}


================================================
FILE: config/index.js
================================================
import common from './env/common'

const env = process.env.NODE_ENV || 'development'
const config = require(`./env/${env}`).default

export default Object.assign({}, common, config)


================================================
FILE: config/passport.js
================================================
import passport from 'koa-passport'
import User from '../src/models/users'
import { Strategy } from 'passport-local'

passport.serializeUser((user, done) => {
  done(null, user.id)
})

passport.deserializeUser(async (id, done) => {
  try {
    const user = await User.findById(id, '-password')
    done(null, user)
  } catch (err) {
    done(err)
  }
})

passport.use('local', new Strategy({
  usernameField: 'username',
  passwordField: 'password'
}, async (username, password, done) => {
  try {
    const user = await User.findOne({ username })
    if (!user) { return done(null, false) }

    try {
      const isMatch = await user.validatePassword(password)

      if (!isMatch) { return done(null, false) }

      done(null, user)
    } catch (err) {
      done(err)
    }

  } catch (err) {
    return done(err)
  }
}))


================================================
FILE: index.js
================================================
require('babel-core/register')()
require('babel-polyfill')
require('./bin/server.js')


================================================
FILE: package.json
================================================
{
  "name": "koa2-api-boilerplate",
  "version": "2.2.0",
  "description": "Koa2 boilerplate covering essentials for APIs",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "./node_modules/.bin/nodemon index.js",
    "test": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-register --require babel-polyfill",
    "lint": "eslint src/**/*.js",
    "docs": "./node_modules/.bin/apidoc -i src/ -o docs"
  },
  "keywords": [
    "koa2-api-boilerplate",
    "api",
    "koa",
    "koa2",
    "boilerplate",
    "es6",
    "mongoose",
    "passportjs",
    "apidoc"
  ],
  "author": "Adrian Obelmejias <adrian@obel.me>",
  "license": "MIT",
  "apidoc": {
    "title": "koa2-api-boilerplate",
    "url": "localhost:5000"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/adrianObel/koa2-api-boilerplate"
  },
  "dependencies": {
    "apidoc": "^0.16.1",
    "babel-core": "^6.5.1",
    "babel-polyfill": "^6.5.0",
    "babel-preset-es2015-node5": "^1.2.0",
    "babel-preset-stage-0": "^6.5.0",
    "bcrypt": "^0.8.5",
    "glob": "^7.0.0",
    "jsonwebtoken": "^7.1.9",
    "koa": "^2.0.0-alpha.6",
    "koa-bodyparser": "^3.0.0",
    "koa-convert": "^1.2.0",
    "koa-generic-session": "^1.10.1",
    "koa-logger": "^2.0.0",
    "koa-mount": "^1.3.0",
    "koa-passport": "^2.0.1",
    "koa-router": "^7.0.1",
    "koa-static": "^2.0.0",
    "mongoose": "^4.4.3",
    "passport-local": "^1.0.0"
  },
  "devDependencies": {
    "babel-eslint": "^6.0.2",
    "babel-register": "^6.5.1",
    "chai": "^3.5.0",
    "eslint": "^3.4.0",
    "eslint-config-standard": "^6.0.0",
    "eslint-plugin-promise": "^2.0.1",
    "eslint-plugin-standard": "^2.0.0",
    "mocha": "^3.0.2",
    "nodemon": "^1.8.1",
    "supertest": "^2.0.0"
  }
}


================================================
FILE: src/middleware/index.js
================================================
export function errorMiddleware () {
  return async (ctx, next) => {
    try {
      await next()
    } catch (err) {
      ctx.status = err.status || 500
      ctx.body = err.message
      ctx.app.emit('error', err, ctx)
    }
  }
}


================================================
FILE: src/middleware/validators.js
================================================
import User from '../models/users'
import config from '../../config'
import { getToken } from '../utils/auth'
import { verify } from 'jsonwebtoken'

export async function ensureUser (ctx, next) {
  const token = getToken(ctx)

  if (!token) {
    ctx.throw(401)
  }

  let decoded = null
  try {
    decoded = verify(token, config.token)
  } catch (err) {
    ctx.throw(401)
  }

  ctx.state.user = await User.findById(decoded.id, '-password')
  if (!ctx.state.user) {
    ctx.throw(401)
  }

  return next()
}


================================================
FILE: src/models/users.js
================================================
import mongoose from 'mongoose'
import bcrypt from 'bcrypt'
import config from '../../config'
import jwt from 'jsonwebtoken'

const User = new mongoose.Schema({
  type: { type: String, default: 'User' },
  name: { type: String },
  username: { type: String, required: true, unique: true },
  password: { type: String, required: true }
})

User.pre('save', function preSave (next) {
  const user = this

  if (!user.isModified('password')) {
    return next()
  }

  new Promise((resolve, reject) => {
    bcrypt.genSalt(10, (err, salt) => {
      if (err) { return reject(err) }
      resolve(salt)
    })
  })
  .then(salt => {
    bcrypt.hash(user.password, salt, (err, hash) => {
      if (err) { throw new Error(err) }

      user.password = hash

      next(null)
    })
  })
  .catch(err => next(err))
})

User.methods.validatePassword = function validatePassword (password) {
  const user = this

  return new Promise((resolve, reject) => {
    bcrypt.compare(password, user.password, (err, isMatch) => {
      if (err) { return reject(err) }

      resolve(isMatch)
    })
  })
}

User.methods.generateToken = function generateToken () {
  const user = this

  return jwt.sign({ id: user.id }, config.token)
}

export default mongoose.model('user', User)


================================================
FILE: src/modules/auth/controller.js
================================================
import passport from 'koa-passport'

/**
 * @apiDefine TokenError
 * @apiError Unauthorized Invalid JWT token
 *
 * @apiErrorExample {json} Unauthorized-Error:
 *     HTTP/1.1 401 Unauthorized
 *     {
 *       "status": 401,
 *       "error": "Unauthorized"
 *     }
 */

/**
 * @api {post} /auth Authenticate user
 * @apiVersion 1.0.0
 * @apiName AuthUser
 * @apiGroup Auth
 *
 * @apiParam {String} username  User username.
 * @apiParam {String} password  User password.
 *
 * @apiExample Example usage:
 * curl -H "Content-Type: application/json" -X POST -d '{ "username": "johndoe@gmail.com", "password": "foo" }' localhost:5000/auth
 *
 * @apiSuccess {Object}   user           User object
 * @apiSuccess {ObjectId} user._id       User id
 * @apiSuccess {String}   user.name      User name
 * @apiSuccess {String}   user.username  User username
 * @apiSuccess {String}   token          Encoded JWT
 *
 * @apiSuccessExample {json} Success-Response:
 *     HTTP/1.1 200 OK
 *     {
 *       "user": {
 *          "_id": "56bd1da600a526986cf65c80"
 *          "username": "johndoe"
 *        },
 *       "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
 *     }
 *
 * @apiError Unauthorized Incorrect credentials
 *
 * @apiErrorExample {json} Error-Response:
 *     HTTP/1.1 401 Unauthorized
 *     {
 *       "status": 401,
 *       "error": "Unauthorized"
 *     }
 */

export async function authUser (ctx, next) {
  return passport.authenticate('local', (user) => {
    if (!user) {
      ctx.throw(401)
    }

    const token = user.generateToken()

    const response = user.toJSON()

    delete response.password

    ctx.body = {
      token,
      user: response
    }
  })(ctx, next)
}


================================================
FILE: src/modules/auth/router.js
================================================
import * as auth from './controller'

export const baseUrl = '/auth'

export default [
  {
    method: 'POST',
    route: '/',
    handlers: [
      auth.authUser
    ]
  }
]


================================================
FILE: src/modules/index.js
================================================
import glob from 'glob'
import Router from 'koa-router'

exports = module.exports = function initModules (app) {
  glob(`${__dirname}/*`, { ignore: '**/index.js' }, (err, matches) => {
    if (err) { throw err }

    matches.forEach((mod) => {
      const router = require(`${mod}/router`)

      const routes = router.default
      const baseUrl = router.baseUrl
      const instance = new Router({ prefix: baseUrl })

      routes.forEach((config) => {
        const {
          method = '',
          route = '',
          handlers = []
        } = config

        const lastHandler = handlers.pop()

        instance[method.toLowerCase()](route, ...handlers, async function(ctx) {
          return await lastHandler(ctx)
        })

        app
          .use(instance.routes())
          .use(instance.allowedMethods())
      })
    })
  })
}


================================================
FILE: src/modules/users/controller.js
================================================
import User from '../../models/users'

/**
 * @api {post} /users Create a new user
 * @apiPermission
 * @apiVersion 1.0.0
 * @apiName CreateUser
 * @apiGroup Users
 *
 * @apiExample Example usage:
 * curl -H "Content-Type: application/json" -X POST -d '{ "user": { "username": "johndoe", "password": "secretpasas" } }' localhost:5000/users
 *
 * @apiParam {Object} user          User object (required)
 * @apiParam {String} user.username Username.
 * @apiParam {String} user.password Password.
 *
 * @apiSuccess {Object}   users           User object
 * @apiSuccess {ObjectId} users._id       User id
 * @apiSuccess {String}   users.name      User name
 * @apiSuccess {String}   users.username  User username
 *
 * @apiSuccessExample {json} Success-Response:
 *     HTTP/1.1 200 OK
 *     {
 *       "user": {
 *          "_id": "56bd1da600a526986cf65c80"
 *          "name": "John Doe"
 *          "username": "johndoe"
 *       }
 *     }
 *
 * @apiError UnprocessableEntity Missing required parameters
 *
 * @apiErrorExample {json} Error-Response:
 *     HTTP/1.1 422 Unprocessable Entity
 *     {
 *       "status": 422,
 *       "error": "Unprocessable Entity"
 *     }
 */
export async function createUser (ctx) {
  const user = new User(ctx.request.body.user)
  try {
    await user.save()
  } catch (err) {
    ctx.throw(422, err.message)
  }

  const token = user.generateToken()
  const response = user.toJSON()

  delete response.password

  ctx.body = {
    user: response,
    token
  }
}

/**
 * @api {get} /users Get all users
 * @apiPermission user
 * @apiVersion 1.0.0
 * @apiName GetUsers
 * @apiGroup Users
 *
 * @apiExample Example usage:
 * curl -H "Content-Type: application/json" -X GET localhost:5000/users
 *
 * @apiSuccess {Object[]} users           Array of user objects
 * @apiSuccess {ObjectId} users._id       User id
 * @apiSuccess {String}   users.name      User name
 * @apiSuccess {String}   users.username  User username
 *
 * @apiSuccessExample {json} Success-Response:
 *     HTTP/1.1 200 OK
 *     {
 *       "users": [{
 *          "_id": "56bd1da600a526986cf65c80"
 *          "name": "John Doe"
 *          "username": "johndoe"
 *       }]
 *     }
 *
 * @apiUse TokenError
 */
export async function getUsers (ctx) {
  const users = await User.find({}, '-password')
  ctx.body = { users }
}

/**
 * @api {get} /users/:id Get user by id
 * @apiPermission user
 * @apiVersion 1.0.0
 * @apiName GetUser
 * @apiGroup Users
 *
 * @apiExample Example usage:
 * curl -H "Content-Type: application/json" -X GET localhost:5000/users/56bd1da600a526986cf65c80
 *
 * @apiSuccess {Object}   users           User object
 * @apiSuccess {ObjectId} users._id       User id
 * @apiSuccess {String}   users.name      User name
 * @apiSuccess {String}   users.username  User username
 *
 * @apiSuccessExample {json} Success-Response:
 *     HTTP/1.1 200 OK
 *     {
 *       "user": {
 *          "_id": "56bd1da600a526986cf65c80"
 *          "name": "John Doe"
 *          "username": "johndoe"
 *       }
 *     }
 *
 * @apiUse TokenError
 */
export async function getUser (ctx, next) {
  try {
    const user = await User.findById(ctx.params.id, '-password')
    if (!user) {
      ctx.throw(404)
    }

    ctx.body = {
      user
    }
  } catch (err) {
    if (err === 404 || err.name === 'CastError') {
      ctx.throw(404)
    }

    ctx.throw(500)
  }

  if (next) { return next() }
}

/**
 * @api {put} /users/:id Update a user
 * @apiPermission
 * @apiVersion 1.0.0
 * @apiName UpdateUser
 * @apiGroup Users
 *
 * @apiExample Example usage:
 * curl -H "Content-Type: application/json" -X PUT -d '{ "user": { "name": "Cool new Name" } }' localhost:5000/users/56bd1da600a526986cf65c80
 *
 * @apiParam {Object} user          User object (required)
 * @apiParam {String} user.name     Name.
 * @apiParam {String} user.username Username.
 *
 * @apiSuccess {Object}   users           User object
 * @apiSuccess {ObjectId} users._id       User id
 * @apiSuccess {String}   users.name      Updated name
 * @apiSuccess {String}   users.username  Updated username
 *
 * @apiSuccessExample {json} Success-Response:
 *     HTTP/1.1 200 OK
 *     {
 *       "user": {
 *          "_id": "56bd1da600a526986cf65c80"
 *          "name": "Cool new name"
 *          "username": "johndoe"
 *       }
 *     }
 *
 * @apiError UnprocessableEntity Missing required parameters
 *
 * @apiErrorExample {json} Error-Response:
 *     HTTP/1.1 422 Unprocessable Entity
 *     {
 *       "status": 422,
 *       "error": "Unprocessable Entity"
 *     }
 *
 * @apiUse TokenError
 */
export async function updateUser (ctx) {
  const user = ctx.body.user

  Object.assign(user, ctx.request.body.user)

  await user.save()

  ctx.body = {
    user
  }
}

/**
 * @api {delete} /users/:id Delete a user
 * @apiPermission
 * @apiVersion 1.0.0
 * @apiName DeleteUser
 * @apiGroup Users
 *
 * @apiExample Example usage:
 * curl -H "Content-Type: application/json" -X DELETE localhost:5000/users/56bd1da600a526986cf65c80
 *
 * @apiSuccess {StatusCode} 200
 *
 * @apiSuccessExample {json} Success-Response:
 *     HTTP/1.1 200 OK
 *     {
 *       "success": true
 *     }
 *
 * @apiUse TokenError
 */

export async function deleteUser (ctx) {
  const user = ctx.body.user

  await user.remove()

  ctx.status = 200
  ctx.body = {
    success: true
  }
}


================================================
FILE: src/modules/users/router.js
================================================
import { ensureUser } from '../../middleware/validators'
import * as user from './controller'

export const baseUrl = '/users'

export default [
  {
    method: 'POST',
    route: '/',
    handlers: [
      user.createUser
    ]
  },
  {
    method: 'GET',
    route: '/',
    handlers: [
      ensureUser,
      user.getUsers
    ]
  },
  {
    method: 'GET',
    route: '/:id',
    handlers: [
      ensureUser,
      user.getUser
    ]
  },
  {
    method: 'PUT',
    route: '/:id',
    handlers: [
      ensureUser,
      user.getUser,
      user.updateUser
    ]
  },
  {
    method: 'DELETE',
    route: '/:id',
    handlers: [
      ensureUser,
      user.getUser,
      user.deleteUser
    ]
  }
]


================================================
FILE: src/utils/auth.js
================================================
export function getToken (ctx) {
  const header = ctx.request.header.authorization
  if (!header) {
    return null
  }
  const parts = header.split(' ')
  if (parts.length !== 2) {
    return null
  }
  const scheme = parts[0]
  const token = parts[1]
  if (/^Bearer$/i.test(scheme)) {
    return token
  }
  return null
}


================================================
FILE: test/auth.spec.js
================================================
import app from '../bin/server'
import supertest from 'supertest'
import { expect, should } from 'chai'
import { cleanDb, authUser } from './utils'

should()
const request = supertest.agent(app.listen())
const context = {}

describe('Auth', () => {
  before((done) => {
    cleanDb()
    authUser(request, (err, { user, token }) => {
      if (err) { return done(err) }

      context.user = user
      context.token = token
      done()
    })
  })

  describe('POST /auth', () => {
    it('should throw 401 if credentials are incorrect', (done) => {
      request
        .post('/auth')
        .set('Accept', 'application/json')
        .send({ username: 'supercoolname', password: 'wrongpassword' })
        .expect(401, done)
    })

    it('should auth user', (done) => {
      request
        .post('/auth')
        .set('Accept', 'application/json')
        .send({ username: 'test', password: 'pass' })
        .expect(200, (err, res) => {
          if (err) { return done(err) }

          res.body.user.should.have.property('username')
          res.body.user.username.should.equal('test')
          expect(res.body.user.password).to.not.exist

          context.user = res.body.user
          context.token = res.body.token

          done()
        })
    })
  })
})


================================================
FILE: test/users.spec.js
================================================
import app from '../bin/server'
import supertest from 'supertest'
import { expect, should } from 'chai'
import { cleanDb } from './utils'

should()
const request = supertest.agent(app.listen())
const context = {}

describe('Users', () => {
  before((done) => {
    cleanDb()
    done()
  })

  describe('POST /users', () => {
    it('should reject signup when data is incomplete', (done) => {
      request
        .post('/users')
        .set('Accept', 'application/json')
        .send({ username: 'supercoolname' })
        .expect(422, done)
    })

    it('should sign up', (done) => {
      request
        .post('/users')
        .set('Accept', 'application/json')
        .send({ user: { username: 'supercoolname', password: 'supersecretpassword' } })
        .expect(200, (err, res) => {
          if (err) { return done(err) }

          res.body.user.should.have.property('username')
          res.body.user.username.should.equal('supercoolname')
          expect(res.body.user.password).to.not.exist

          context.user = res.body.user
          context.token = res.body.token

          done()
        })
    })
  })

  describe('GET /users', () => {
    it('should not fetch users if the authorization header is missing', (done) => {
      request
        .get('/users')
        .set('Accept', 'application/json')
        .expect(401, done)
    })

    it('should not fetch users if the authorization header is missing the scheme', (done) => {
      request
        .get('/users')
        .set({
          Accept: 'application/json',
          Authorization: '1'
        })
        .expect(401, done)
    })

    it('should not fetch users if the authorization header has invalid scheme', (done) => {
      const { token } = context
      request
        .get('/users')
        .set({
          Accept: 'application/json',
          Authorization: `Unknown ${token}`
        })
        .expect(401, done)
    })

    it('should not fetch users if token is invalid', (done) => {
      request
        .get('/users')
        .set({
          Accept: 'application/json',
          Authorization: 'Bearer 1'
        })
        .expect(401, done)
    })

    it('should fetch all users', (done) => {
      const { token } = context
      request
        .get('/users')
        .set({
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        })
        .expect(200, (err, res) => {
          if (err) { return done(err) }

          res.body.should.have.property('users')

          res.body.users.should.have.length(1)

          done()
        })
    })
  })

  describe('GET /users/:id', () => {
    it('should not fetch user if token is invalid', (done) => {
      request
        .get('/users/1')
        .set({
          Accept: 'application/json',
          Authorization: 'Bearer 1'
        })
        .expect(401, done)
    })

    it('should throw 404 if user doesn\'t exist', (done) => {
      const { token } = context
      request
        .get('/users/1')
        .set({
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        })
        .expect(404, done)
    })

    it('should fetch user', (done) => {
      const {
        user: { _id },
        token
      } = context

      request
        .get(`/users/${_id}`)
        .set({
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        })
        .expect(200, (err, res) => {
          if (err) { return done(err) }

          res.body.should.have.property('user')

          expect(res.body.user.password).to.not.exist

          done()
        })
    })
  })

  describe('PUT /users/:id', () => {
    it('should not update user if token is invalid', (done) => {
      request
        .put('/users/1')
        .set({
          Accept: 'application/json',
          Authorization: 'Bearer 1'
        })
        .expect(401, done)
    })

    it('should throw 404 if user doesn\'t exist', (done) => {
      const { token } = context
      request
        .put('/users/1')
        .set({
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        })
        .expect(404, done)
    })

    it('should update user', (done) => {
      const {
        user: { _id },
        token
      } = context

      request
        .put(`/users/${_id}`)
        .set({
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        })
        .send({ user: { username: 'updatedcoolname' } })
        .expect(200, (err, res) => {
          if (err) { return done(err) }

          res.body.user.should.have.property('username')
          res.body.user.username.should.equal('updatedcoolname')
          expect(res.body.user.password).to.not.exist

          done()
        })
    })
  })

  describe('DELETE /users/:id', () => {
    it('should not delete user if token is invalid', (done) => {
      request
        .delete('/users/1')
        .set({
          Accept: 'application/json',
          Authorization: 'Bearer 1'
        })
        .expect(401, done)
    })

    it('should throw 404 if user doesn\'t exist', (done) => {
      const { token } = context
      request
        .delete('/users/1')
        .set({
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        })
        .expect(404, done)
    })

    it('should delete user', (done) => {
      const {
        user: { _id },
        token
      } = context

      request
        .delete(`/users/${_id}`)
        .set({
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        })
        .expect(200, done)
    })
  })
})


================================================
FILE: test/utils.js
================================================
import mongoose from 'mongoose'

export function cleanDb () {
  for (const collection in mongoose.connection.collections) {
    if (mongoose.connection.collections.hasOwnProperty(collection)) {
      mongoose.connection.collections[collection].remove()
    }
  }
}

export function authUser (agent, callback) {
  agent
    .post('/users')
    .set('Accept', 'application/json')
    .send({ user: { username: 'test', password: 'pass' } })
    .end((err, res) => {
      if (err) { return callback(err) }

      callback(null, {
        user: res.body.user,
        token: res.body.token
      })
    })
}
Download .txt
gitextract_sjp2bf63/

├── .babelrc
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── bin/
│   └── server.js
├── config/
│   ├── env/
│   │   ├── common.js
│   │   ├── development.js
│   │   ├── production.js
│   │   └── test.js
│   ├── index.js
│   └── passport.js
├── index.js
├── package.json
├── src/
│   ├── middleware/
│   │   ├── index.js
│   │   └── validators.js
│   ├── models/
│   │   └── users.js
│   ├── modules/
│   │   ├── auth/
│   │   │   ├── controller.js
│   │   │   └── router.js
│   │   ├── index.js
│   │   └── users/
│   │       ├── controller.js
│   │       └── router.js
│   └── utils/
│       └── auth.js
└── test/
    ├── auth.spec.js
    ├── users.spec.js
    └── utils.js
Download .txt
SYMBOL INDEX (11 symbols across 6 files)

FILE: src/middleware/index.js
  function errorMiddleware (line 1) | function errorMiddleware () {

FILE: src/middleware/validators.js
  function ensureUser (line 6) | async function ensureUser (ctx, next) {

FILE: src/modules/auth/controller.js
  function authUser (line 53) | async function authUser (ctx, next) {

FILE: src/modules/users/controller.js
  function createUser (line 41) | async function createUser (ctx) {
  function getUsers (line 87) | async function getUsers (ctx) {
  function getUser (line 119) | async function getUser (ctx, next) {
  function updateUser (line 180) | async function updateUser (ctx) {
  function deleteUser (line 213) | async function deleteUser (ctx) {

FILE: src/utils/auth.js
  function getToken (line 1) | function getToken (ctx) {

FILE: test/utils.js
  function cleanDb (line 3) | function cleanDb () {
  function authUser (line 11) | function authUser (agent, callback) {
Condensed preview — 27 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (31K chars).
[
  {
    "path": ".babelrc",
    "chars": 57,
    "preview": "{\n  \"presets\": [\n    \"es2015-node5\",\n    \"stage-0\"\n  ]\n}\n"
  },
  {
    "path": ".editorconfig",
    "chars": 781,
    "preview": "# http://editorconfig.org\n\n# A special property that should be specified at the top of the file outside of\n# any section"
  },
  {
    "path": ".eslintrc.json",
    "chars": 108,
    "preview": "{\n  \"parser\": \"babel-eslint\",\n  \"extends\": \"standard\",\n  \"env\": {\n    \"node\": true,\n    \"mocha\": true\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "chars": 999,
    "preview": "\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 d"
  },
  {
    "path": "LICENSE",
    "chars": 1099,
    "preview": "The MIT License (MIT)\nCopyright (c) 2016 Adrian Obelmejias <adrian@obel.me>\n\nPermission is hereby granted, free of charg"
  },
  {
    "path": "README.md",
    "chars": 2307,
    "preview": "#koa2-api-boilerplate\n[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://"
  },
  {
    "path": "bin/server.js",
    "chars": 917,
    "preview": "import Koa from 'koa'\nimport bodyParser from 'koa-bodyparser'\nimport convert from 'koa-convert'\nimport logger from 'koa-"
  },
  {
    "path": "config/env/common.js",
    "chars": 52,
    "preview": "export default {\n  port: process.env.PORT || 5000\n}\n"
  },
  {
    "path": "config/env/development.js",
    "chars": 148,
    "preview": "export default {\n  session: 'secret-boilerplate-token',\n  token: 'secret-jwt-token',\n  database: 'mongodb://localhost:27"
  },
  {
    "path": "config/env/production.js",
    "chars": 149,
    "preview": "export default {\n  session: 'secret-boilerplate-token',\n  token: 'secret-jwt-token',\n  database: 'mongodb://localhost:27"
  },
  {
    "path": "config/env/test.js",
    "chars": 149,
    "preview": "export default {\n  session: 'secret-boilerplate-token',\n  token: 'secret-jwt-token',\n  database: 'mongodb://localhost:27"
  },
  {
    "path": "config/index.js",
    "chars": 182,
    "preview": "import common from './env/common'\n\nconst env = process.env.NODE_ENV || 'development'\nconst config = require(`./env/${env"
  },
  {
    "path": "config/passport.js",
    "chars": 827,
    "preview": "import passport from 'koa-passport'\nimport User from '../src/models/users'\nimport { Strategy } from 'passport-local'\n\npa"
  },
  {
    "path": "index.js",
    "chars": 86,
    "preview": "require('babel-core/register')()\nrequire('babel-polyfill')\nrequire('./bin/server.js')\n"
  },
  {
    "path": "package.json",
    "chars": 1791,
    "preview": "{\n  \"name\": \"koa2-api-boilerplate\",\n  \"version\": \"2.2.0\",\n  \"description\": \"Koa2 boilerplate covering essentials for API"
  },
  {
    "path": "src/middleware/index.js",
    "chars": 234,
    "preview": "export function errorMiddleware () {\n  return async (ctx, next) => {\n    try {\n      await next()\n    } catch (err) {\n  "
  },
  {
    "path": "src/middleware/validators.js",
    "chars": 511,
    "preview": "import User from '../models/users'\nimport config from '../../config'\nimport { getToken } from '../utils/auth'\nimport { v"
  },
  {
    "path": "src/models/users.js",
    "chars": 1263,
    "preview": "import mongoose from 'mongoose'\nimport bcrypt from 'bcrypt'\nimport config from '../../config'\nimport jwt from 'jsonwebto"
  },
  {
    "path": "src/modules/auth/controller.js",
    "chars": 1808,
    "preview": "import passport from 'koa-passport'\n\n/**\n * @apiDefine TokenError\n * @apiError Unauthorized Invalid JWT token\n *\n * @api"
  },
  {
    "path": "src/modules/auth/router.js",
    "chars": 175,
    "preview": "import * as auth from './controller'\n\nexport const baseUrl = '/auth'\n\nexport default [\n  {\n    method: 'POST',\n    route"
  },
  {
    "path": "src/modules/index.js",
    "chars": 848,
    "preview": "import glob from 'glob'\nimport Router from 'koa-router'\n\nexports = module.exports = function initModules (app) {\n  glob("
  },
  {
    "path": "src/modules/users/controller.js",
    "chars": 5349,
    "preview": "import User from '../../models/users'\n\n/**\n * @api {post} /users Create a new user\n * @apiPermission\n * @apiVersion 1.0."
  },
  {
    "path": "src/modules/users/router.js",
    "chars": 706,
    "preview": "import { ensureUser } from '../../middleware/validators'\nimport * as user from './controller'\n\nexport const baseUrl = '/"
  },
  {
    "path": "src/utils/auth.js",
    "chars": 324,
    "preview": "export function getToken (ctx) {\n  const header = ctx.request.header.authorization\n  if (!header) {\n    return null\n  }\n"
  },
  {
    "path": "test/auth.spec.js",
    "chars": 1280,
    "preview": "import app from '../bin/server'\nimport supertest from 'supertest'\nimport { expect, should } from 'chai'\nimport { cleanDb"
  },
  {
    "path": "test/users.spec.js",
    "chars": 5673,
    "preview": "import app from '../bin/server'\nimport supertest from 'supertest'\nimport { expect, should } from 'chai'\nimport { cleanDb"
  },
  {
    "path": "test/utils.js",
    "chars": 604,
    "preview": "import mongoose from 'mongoose'\n\nexport function cleanDb () {\n  for (const collection in mongoose.connection.collections"
  }
]

About this extraction

This page contains the full source code of the adrianObel/koa2-api-boilerplate GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 27 files (27.8 KB), approximately 8.3k tokens, and a symbol index with 11 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!