forked from koa-boilerplate

This commit is contained in:
Chris Troutner
2019-01-20 18:12:23 -08:00
commit 44b519bfcb
30 changed files with 1577 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
# 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
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "standard",
"env": {
"node": true,
"mocha": true
}
}
+61
View File
@@ -0,0 +1,61 @@
# 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
.nyc_output
coverage
+1
View File
@@ -0,0 +1 @@
package-lock = false
+27
View File
@@ -0,0 +1,27 @@
# This is a node.js v8+ JavaScript project
language: node_js
node_js:
- "8"
# Build on Ubuntu Trusty (14.04)
# https://docs.travis-ci.com/user/reference/trusty/#javascript-and-nodejs-images
dist: trusty
sudo: required
# Use Docker
services:
- docker
before_install:
- ./install-mongo
#- npm install -g mocha
# Send coverage data to Coveralls
after_success:
- npm run coverage
deploy:
provider: script
skip_cleanup: true
script:
- npx semantic-release
+8
View File
@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright (c) 2016 Adrian Obelmejias <adrian@obel.me> & Chris Troutner <chris.troutner@gmail.com>
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.
+91
View File
@@ -0,0 +1,91 @@
# babel-free-koa2-api-boilerplate
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com)
[![Coverage Status](https://coveralls.io/repos/github/christroutner/babel-free-koa2-api-boilerplate/badge.svg?branch=unstable)](https://coveralls.io/github/christroutner/babel-free-koa2-api-boilerplate?branch=unstable)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![Greenkeeper badge](https://badges.greenkeeper.io/christroutner/babel-free-koa2-api-boilerplate.svg)](https://greenkeeper.io/)
Boilerplate for building APIs with [koa2](https://github.com/koajs/koa/tree/v2.x) and mongodb.
This repository is forked from Adrian Obelmejias' [koa-api-boilerplate repository](https://github.com/adrianObel/koa2-api-boilerplate).
It makes the following modifications:
- Remove babel as a dependency. This repository is now natively compatible with
node v8.9 or higher.
- Replaced `bcrypt` dependency with `bcryptjs`. This improves compatibility across
versions of node.js and across OSs.
- Configured for Travis CI (continuous integration), Coveralls (code coverage), GreenKeeper (automated dependency management), and Semantic Release (automated versioning).
## Features
This project covers basic necessities of most APIs.
* Authentication (passport & jwt)
* Database (mongoose)
* Testing (mocha)
* Doc generation with apidoc
* linting using standard
## Requirements
* node __^8.9.4__
* npm __^5.7.1__
## Installation
```bash
git clone https://github.com/christroutner/babel-free-koa2-api-boilerplate.git
```
## 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
## Dependencies
* [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/)
* [ESLint](http://eslint.org/)
## License
MIT
+63
View File
@@ -0,0 +1,63 @@
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const convert = require('koa-convert')
const logger = require('koa-logger')
const mongoose = require('mongoose')
const session = require('koa-generic-session')
const passport = require('koa-passport')
const mount = require('koa-mount')
const serve = require('koa-static')
const cors = require('kcors')
const config = require('../config')
const errorMiddleware = require('../src/middleware')
async function startServer () {
// Create a Koa instance.
const app = new Koa()
app.keys = [config.session]
// Connect to the Mongo Database.
mongoose.Promise = global.Promise
await mongoose.connect(config.database, { useNewUrlParser: true })
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
// MIDDLEWARE START
app.use(convert(logger()))
app.use(bodyParser())
app.use(session())
app.use(errorMiddleware())
// Used to generate the docs.
app.use(convert(mount('/docs', serve(`${process.cwd()}/docs`))))
// User Authentication
require('../config/passport')
app.use(passport.initialize())
app.use(passport.session())
// Custom Middleware Modules
const modules = require('../src/modules')
modules(app)
// Enable CORS for testing
// app.use(cors({origin: '*'}))
// MIDDLEWARE END
// app.listen(config.port, () => {
// console.log(`Server started on ${config.port}`)
// })
await app.listen(config.port)
console.log(`Server started on ${config.port}`)
return app
}
// startServer()
// export default app
// module.exports = app
module.exports = {
startServer
}
+7
View File
@@ -0,0 +1,7 @@
// export default {
// port: process.env.PORT || 5000
// }
module.exports = {
port: process.env.PORT || 5000
}
+13
View File
@@ -0,0 +1,13 @@
/*
export default {
session: 'secret-boilerplate-token',
token: 'secret-jwt-token',
database: 'mongodb://localhost:27017/p2pvps-server-dev'
}
*/
module.exports = {
session: 'secret-boilerplate-token',
token: 'secret-jwt-token',
database: 'mongodb://localhost:27017/p2pvps-server-dev'
}
+13
View File
@@ -0,0 +1,13 @@
/*
export default {
session: 'secret-boilerplate-token',
token: 'secret-jwt-token',
database: 'mongodb://localhost:27017/p2pvps-server-prod'
}
*/
module.exports = {
session: 'secret-boilerplate-token',
token: 'secret-jwt-token',
database: 'mongodb://localhost:27017/p2pvps-server-prod'
}
+13
View File
@@ -0,0 +1,13 @@
/*
export default {
session: 'secret-boilerplate-token',
token: 'secret-jwt-token',
database: 'mongodb://localhost:27017/p2pvps-server-test'
}
*/
module.exports = {
session: 'secret-boilerplate-token',
token: 'secret-jwt-token',
database: 'mongodb://localhost:27017/p2pvps-server-test'
}
+6
View File
@@ -0,0 +1,6 @@
const common = require('./env/common')
const env = process.env.NODE_ENV || 'development'
const config = require(`./env/${env}`)
module.exports = Object.assign({}, common, config)
+39
View File
@@ -0,0 +1,39 @@
const passport = require('koa-passport')
const User = require('../src/models/users')
const Strategy = require('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)
}
}))
+3
View File
@@ -0,0 +1,3 @@
const server = require('./bin/server.js')
server.startServer()
Executable
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6
echo "deb [ arch=amd64,arm64 ] http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list
sudo apt-get update
sudo apt-get install -y mongodb-org
sudo service mongod start
+74
View File
@@ -0,0 +1,74 @@
{
"name": "babel-free-koa2-api-boilerplate",
"version": "3.0.0",
"description": "Koa2 boilerplate covering essentials for APIs, babel removed as a dependency",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "NODE_ENV=test nyc --reporter=text ./node_modules/.bin/mocha --exit",
"lint": "eslint src/**/*.js",
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"coverage:report": "nyc --reporter=html mocha --exit"
},
"keywords": [
"koa2-api-boilerplate",
"api",
"koa",
"koa2",
"boilerplate",
"es6",
"mongoose",
"passportjs",
"apidoc"
],
"author": "Chris Troutner <chris.troutner@gmail.com>",
"license": "MIT",
"apidoc": {
"title": "babel-free-koa2-api-boilerplate",
"url": "localhost:5000"
},
"repository": "christroutner/babel-free-koa2-api-boilerplate",
"dependencies": {
"apidoc": "^0.17.6",
"bcryptjs": "^2.4.3",
"glob": "^7.0.0",
"jsonwebtoken": "^8.3.0",
"kcors": "^2.2.1",
"koa": "^2.5.0",
"koa-bodyparser": "^4.2.0",
"koa-convert": "^1.2.0",
"koa-generic-session": "^2.0.1",
"koa-logger": "^3.1.0",
"koa-mount": "^4.0.0",
"koa-passport": "^4.1.1",
"koa-router": "^7.0.1",
"koa-static": "^5.0.0",
"mongoose": "^5.0.8",
"passport-local": "^1.0.0",
"request": "^2.85.0",
"request-promise": "^4.2.2"
},
"devDependencies": {
"chai": "^4.1.2",
"coveralls": "^3.0.2",
"eslint": "^5.8.0",
"eslint-config-promise": "^1.2.6",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0",
"mocha": "^5.2.0",
"nyc": "^13.1.0",
"semantic-release": "^15.10.8"
},
"release": {
"publish": [
{
"path": "@semantic-release/npm",
"npmPublish": false
}
]
}
}
+11
View File
@@ -0,0 +1,11 @@
module.exports = 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)
}
}
}
+32
View File
@@ -0,0 +1,32 @@
const User = require('../models/users')
const config = require('../../config')
const getToken = require('../utils/auth')
const jwt = require('jsonwebtoken')
module.exports = async function ensureUser (ctx, next) {
//console.log(`getToken: ${typeof (getToken)}`)
const token = getToken(ctx)
if (!token) {
//console.log(`Err: Token not provided.`)
ctx.throw(401)
}
let decoded = null
try {
//console.log(`token: ${JSON.stringify(token, null, 2)}`)
//console.log(`config: ${JSON.stringify(config, null, 2)}`)
decoded = jwt.verify(token, config.token)
} catch (err) {
//console.log(`Err: Token could not be decoded: ${err}`)
ctx.throw(401)
}
ctx.state.user = await User.findById(decoded.id, '-password')
if (!ctx.state.user) {
//console.log(`Err: Could not find user.`)
ctx.throw(401)
}
return next()
}
+60
View File
@@ -0,0 +1,60 @@
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')
const config = require('../../config')
const jwt = require('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
const token = jwt.sign({ id: user.id }, config.token)
// console.log(`config.token: ${config.token}`)
// console.log(`generated token: ${token}`)
return token
}
// export default mongoose.model('user', User)
module.exports = mongoose.model('user', User)
+74
View File
@@ -0,0 +1,74 @@
const passport = require('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"
* }
*/
async function authUser (ctx, next) {
return passport.authenticate('local', (err, user, info, status) => {
if (err) throw err
if (!user) {
ctx.throw(401)
}
const token = user.generateToken()
const response = user.toJSON()
delete response.password
ctx.body = {
token,
user: response
}
})(ctx, next)
}
module.exports.authUser = authUser
+16
View File
@@ -0,0 +1,16 @@
// import * as auth from './controller'
const auth = require('./controller')
// export const baseUrl = '/auth'
module.exports.baseUrl = '/auth'
// export default [
module.exports.routes = [
{
method: 'POST',
route: '/',
handlers: [
auth.authUser
]
}
]
+46
View File
@@ -0,0 +1,46 @@
const glob = require('glob')
const Router = require('koa-router')
module.exports = function initModules (app) {
glob(`${__dirname}/*`, { ignore: '**/index.js' }, (err, matches) => {
if (err) { throw err }
// Loop through each sub-directory in the modules directory.
matches.forEach((mod) => {
// console.log(`router = ${mod}/router`)
const router = require(`${mod}/router`)
const routes = router.routes
const baseUrl = router.baseUrl
const instance = new Router({ prefix: baseUrl })
// console.log(`routes: ${JSON.stringify(routes, null, 2)}`)
// Loop through each route defined in the router.js file.
routes.forEach((config) => {
// console.log(`modules/index.js config: ${JSON.stringify(config, null, 2)}`)
// const {
// method = '',
// route = '',
// handlers = []
// } = config
const method = config.method || ''
const route = config.route || ''
const handlers = config.handlers || []
const lastHandler = handlers.pop()
instance[method.toLowerCase()](route, ...handlers, async function (ctx) {
// console.log(`typeof lastHandler: ${typeof (lastHandler)}`)
return await lastHandler(ctx)
})
// console.log(`instance: ${JSON.stringify(instance, null, 2)}`)
app
.use(instance.routes())
.use(instance.allowedMethods())
})
})
})
}
+230
View File
@@ -0,0 +1,230 @@
const User = require('../../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"
* }
*/
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
*/
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
*/
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
*/
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
*/
async function deleteUser (ctx) {
const user = ctx.body.user
await user.remove()
ctx.status = 200
ctx.body = {
success: true
}
}
module.exports = {
createUser,
getUsers,
getUser,
updateUser,
deleteUser
}
+49
View File
@@ -0,0 +1,49 @@
const ensureUser = require('../../middleware/validators')
const user = require('./controller')
// export const baseUrl = '/users'
module.exports.baseUrl = '/users'
module.exports.routes = [
{
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
]
}
]
+16
View File
@@ -0,0 +1,16 @@
module.exports = 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
}
+3
View File
@@ -0,0 +1,3 @@
The files in this directory are named so as to control the order in which
the files are executed by mocha. a01... runs first. The order is important,
as some tests downstream depend on tests upstream.
+106
View File
@@ -0,0 +1,106 @@
const app = require('../bin/server')
// const supertest = require('supertest')
// const expect = require('chai').expect
const should = require('chai').should
// const cleanDb = require('./utils').cleanDb
// const authUser = require('./utils').authUser
const utils = require('./utils')
const rp = require('request-promise')
const assert = require('chai').assert
should()
// const request = supertest.agent(app.listen())
const context = {}
const LOCALHOST = 'http://localhost:5000'
describe('Auth', () => {
before(async () => {
await app.startServer()
utils.cleanDb()
/*
authUser(request, (err, { user, token }) => {
if (err) { return done(err) }
context.user = user
context.token = token
done()
})
*/
const userObj = {
username: 'test',
password: 'pass'
}
const testUser = await utils.createUser(userObj)
context.user = testUser.user
context.token = testUser.token
})
describe('POST /auth', () => {
it('should throw 401 if credentials are incorrect', async () => {
try {
const options = {
method: 'POST',
uri: `${LOCALHOST}/auth`,
resolveWithFullResponse: true,
json: true,
body: {
username: 'test',
password: 'wrongpassword'
}
}
let result = await rp(options)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
assert(false, 'Unexpected result')
} catch (err) {
if (err.statusCode === 422) {
assert(err.statusCode === 422, 'Error code 422 expected.')
} else if (err.statusCode === 401) {
assert(err.statusCode === 401, 'Error code 401 expected.')
} else {
console.error('Error: ', err)
console.log('Error stringified: ' + JSON.stringify(err, null, 2))
throw err
}
}
})
it('should auth user', async () => {
try {
const options = {
method: 'POST',
uri: `${LOCALHOST}/auth`,
resolveWithFullResponse: true,
json: true,
body: {
username: 'test',
password: 'pass'
}
}
let result = await rp(options)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert(result.statusCode === 200, 'Status Code 200 expected.')
assert(result.body.user.username === 'test', 'Username of test expected')
assert(result.body.user.password === undefined, 'Password expected to be omited')
} catch (err) {
console.log('Error authenticating test user: ' + JSON.stringify(err, null, 2))
throw err
}
})
})
})
+403
View File
@@ -0,0 +1,403 @@
const expect = require('chai').expect
const should = require('chai').should
const utils = require('./utils')
const rp = require('request-promise')
const assert = require('chai').assert
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const LOCALHOST = 'http://localhost:5000'
should()
const context = {}
describe('Users', () => {
before(async () => {
utils.cleanDb()
})
describe('POST /users', () => {
it('should reject signup when data is incomplete', async () => {
try {
const options = {
method: 'POST',
uri: `${LOCALHOST}/users`,
resolveWithFullResponse: true,
json: true,
body: {
username: 'supercoolname'
}
}
let result = await rp(options)
console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
assert(false, 'Unexpected result')
} catch (err) {
if (err.statusCode === 422) {
assert(err.statusCode === 422, 'Error code 422 expected.')
} else if (err.statusCode === 401) {
assert(err.statusCode === 401, 'Error code 401 expected.')
} else {
console.error('Error: ', err)
console.log('Error stringified: ' + JSON.stringify(err, null, 2))
throw err
}
}
})
it('should sign up', async () => {
try {
const options = {
method: 'POST',
uri: `${LOCALHOST}/users`,
resolveWithFullResponse: true,
json: true,
body: {
user: { username: 'supercoolname', password: 'supersecretpassword' }
}
}
let result = await rp(options)
result.body.user.should.have.property('username')
result.body.user.username.should.equal('supercoolname')
expect(result.body.user.password).to.not.exist
context.user = result.body.user
context.token = result.body.token
} catch (err) {
console.log(
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
)
throw err
}
})
})
describe('GET /users', () => {
it('should not fetch users if the authorization header is missing', async () => {
try {
const options = {
method: 'GET',
uri: `${LOCALHOST}/users`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json'
}
}
await rp(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.statusCode, 401)
}
})
it('should not fetch users if the authorization header is missing the scheme', async () => {
try {
const options = {
method: 'GET',
uri: `${LOCALHOST}/users`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: '1'
}
}
await rp(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.statusCode, 401)
}
})
it('should not fetch users if the authorization header has invalid scheme', async () => {
const { token } = context
try {
const options = {
method: 'GET',
uri: `${LOCALHOST}/users`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Unknown ${token}`
}
}
await rp(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.statusCode, 401)
}
})
it('should not fetch users if token is invalid', async () => {
try {
const options = {
method: 'GET',
uri: `${LOCALHOST}/users`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Bearer 1`
}
}
await rp(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.statusCode, 401)
}
})
it('should fetch all users', async () => {
const { token } = context
const options = {
method: 'GET',
uri: `${LOCALHOST}/users`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
const result = await rp(options)
const users = result.body.users
// console.log(`users: ${util.inspect(users)}`)
assert.hasAnyKeys(users[0], ['type', '_id', 'username'])
assert.equal(users.length, 1)
})
})
describe('GET /users/:id', () => {
it('should not fetch user if token is invalid', async () => {
try {
const options = {
method: 'GET',
uri: `${LOCALHOST}/users/1`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Bearer 1`
}
}
await rp(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.statusCode, 401)
}
})
it("should throw 404 if user doesn't exist", async () => {
const { token } = context
try {
const options = {
method: 'GET',
uri: `${LOCALHOST}/users/1`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
await rp(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.statusCode, 404)
}
})
it('should fetch user', async () => {
const {
user: { _id },
token
} = context
const options = {
method: 'GET',
uri: `${LOCALHOST}/users/${_id}`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
const result = await rp(options)
const user = result.body.user
// console.log(`user: ${util.inspect(user)}`)
assert.hasAnyKeys(user, ['type', '_id', 'username'])
assert.equal(user._id, _id)
assert.notProperty(
user,
'password',
'Password property should not be returned'
)
})
})
describe('PUT /users/:id', () => {
it('should not update user if token is invalid', async () => {
try {
const options = {
method: 'PUT',
uri: `${LOCALHOST}/users/1`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Bearer 1`
}
}
await rp(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.statusCode, 401)
}
})
it("should throw 404 if user doesn't exist", async () => {
const { token } = context
try {
const options = {
method: 'PUT',
uri: `${LOCALHOST}/users/1`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
await rp(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.statusCode, 404)
}
})
it('should update user', async () => {
const {
user: { _id },
token
} = context
const options = {
method: 'PUT',
uri: `${LOCALHOST}/users/${_id}`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
},
body: {
user: { username: 'updatedcoolname' }
}
}
const result = await rp(options)
const user = result.body.user
// console.log(`user: ${util.inspect(user)}`)
assert.hasAnyKeys(user, ['type', '_id', 'username'])
assert.equal(user._id, _id)
assert.notProperty(
user,
'password',
'Password property should not be returned'
)
assert.equal(user.username, 'updatedcoolname')
})
})
describe('DELETE /users/:id', () => {
it('should not delete user if token is invalid', async () => {
try {
const options = {
method: 'DELETE',
uri: `${LOCALHOST}/users/1`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Bearer 1`
}
}
await rp(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.statusCode, 401)
}
})
it('should throw 404 if user doesn\'t exist', async () => {
const { token } = context
try {
const options = {
method: 'DELETE',
uri: `${LOCALHOST}/users/1`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
await rp(options)
assert.equal(true, false, 'Unexpected behavior')
} catch (err) {
assert.equal(err.statusCode, 404)
}
})
it('should delete user', async () => {
const {
user: { _id },
token
} = context
const options = {
method: 'DELETE',
uri: `${LOCALHOST}/users/${_id}`,
resolveWithFullResponse: true,
json: true,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
const result = await rp(options)
// console.log(`result: ${util.inspect(result.body)}`)
assert.equal(result.body.success, true)
})
})
})
+68
View File
@@ -0,0 +1,68 @@
const mongoose = require('mongoose')
const rp = require('request-promise')
const LOCALHOST = 'http://localhost:5000'
// Remove all collections from the DB.
function cleanDb () {
for (const collection in mongoose.connection.collections) {
if (mongoose.connection.collections.hasOwnProperty(collection)) {
mongoose.connection.collections[collection].deleteMany()
}
}
}
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
})
})
}
// This function is used to create new users.
// userObj = {
// username,
// password
// }
async function createUser (userObj) {
try {
const options = {
method: 'POST',
uri: `${LOCALHOST}/users`,
resolveWithFullResponse: true,
json: true,
body: {
user: {
username: userObj.username,
password: userObj.password
}
}
}
let result = await rp(options)
const retObj = {
user: result.body.user,
token: result.body.token
}
return retObj
} catch (err) {
console.log('Error in utils.js/createUser(): ' + JSON.stringify(err, null, 2))
throw err
}
}
module.exports = {
cleanDb,
authUser,
createUser
}