diff --git a/bin/server.js b/bin/server.js index cd425eb..c9b1055 100644 --- a/bin/server.js +++ b/bin/server.js @@ -27,8 +27,8 @@ require('../config/passport') app.use(passport.initialize()) app.use(passport.session()) -const router = require('../src/controllers') -router(app) +const modules = require('../src/modules') +modules(app) app.listen(config.port, () => { console.log(`Server started on ${config.port}`) diff --git a/src/controllers/index.js b/src/controllers/index.js deleted file mode 100644 index a49672d..0000000 --- a/src/controllers/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import glob from 'glob' - -exports = module.exports = function Controllers(app) { - glob(`${__dirname}/*.js`, { ignore: '**/index.js' }, (err, matches) => { - if (err) { throw err } - - matches.forEach((file) => { - const controller = require(file).default - app - .use(controller.routes()) - .use(controller.allowedMethods()) - }) - }) -} diff --git a/src/models/users.js b/src/models/users.js index be2f4f4..a78add1 100644 --- a/src/models/users.js +++ b/src/models/users.js @@ -1,5 +1,7 @@ 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' }, @@ -47,4 +49,10 @@ User.methods.validatePassword = function validatePassword(password) { }) } +User.methods.generateToken = function generateToken() { + const user = this + + return jwt.sign({ id: user.id }, config.token) +} + export default mongoose.model('user', User) diff --git a/src/controllers/auth.js b/src/modules/auth/controller.js similarity index 89% rename from src/controllers/auth.js rename to src/modules/auth/controller.js index bfef202..6965ff5 100644 --- a/src/controllers/auth.js +++ b/src/modules/auth/controller.js @@ -1,9 +1,6 @@ -import Router from 'koa-router' import passport from 'koa-passport' import jwt from 'jsonwebtoken' -import config from '../../config' - -const router = new Router({ prefix: '/auth' }) +import config from '../../../config' /** * @apiDefine TokenError @@ -55,8 +52,9 @@ const router = new Router({ prefix: '/auth' }) * "error": "Unauthorized" * } */ -router.post('/', async (ctx, next) => - passport.authenticate('local', (user) => { + +export async function authUser(ctx, next) { + return passport.authenticate('local', (user) => { if (!user) { ctx.throw(401) } @@ -73,6 +71,4 @@ router.post('/', async (ctx, next) => user: response } })(ctx, next) -) - -export default router +} diff --git a/src/modules/auth/router.js b/src/modules/auth/router.js new file mode 100644 index 0000000..06b1d84 --- /dev/null +++ b/src/modules/auth/router.js @@ -0,0 +1,13 @@ +import * as auth from './controller' + +export const baseUrl = '/auth' + +export default [ + { + method: 'POST', + route: '/', + handlers: [ + auth.authUser + ] + } +] diff --git a/src/modules/index.js b/src/modules/index.js new file mode 100644 index 0000000..6e9dd6e --- /dev/null +++ b/src/modules/index.js @@ -0,0 +1,30 @@ +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 + + instance[method.toLowerCase()](route, ...handlers) + + app + .use(instance.routes()) + .use(instance.allowedMethods()) + }) + }) + }) +} diff --git a/src/controllers/users.js b/src/modules/users/controller.js similarity index 68% rename from src/controllers/users.js rename to src/modules/users/controller.js index 70739e3..f03cf64 100644 --- a/src/controllers/users.js +++ b/src/modules/users/controller.js @@ -1,92 +1,4 @@ -import Router from 'koa-router' -import User from '../models/users' -import config from '../../config' -import jwt from 'jsonwebtoken' -import { ensureUser } from '../middleware/validators' - -const router = new Router({ prefix: '/users' }) - -/** - * @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 - */ -router.get('/', - ensureUser, - async (ctx) => { - const users = await User.find({}, '-password -salt') - 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 - */ -router.get('/:id', - ensureUser, - async (ctx) => { - try { - const user = await User.findById(ctx.params.id, '-password -salt') - if (!user) { - ctx.throw(404) - } - - ctx.body = user - } catch (err) { - if (err === 404 || err.name === 'CastError') { - ctx.throw(404) - } - - ctx.throw(500) - } - } -) +import User from '../../models/users' /** * @api {post} /users Create a new user @@ -126,27 +38,105 @@ router.get('/:id', * "error": "Unprocessable Entity" * } */ -router.post('/', - async (ctx) => { - const user = new User(ctx.request.body.user) - try { - await user.save() - const token = jwt.sign({ id: user.id }, config.token) - - const response = user.toJSON() - - delete response.password - delete response.salt - - ctx.body = { - user: response, - token - } - } catch (err) { - ctx.throw(422, err) - } +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 + delete response.salt + + 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 -salt') + 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 -salt') + if (!user) { + ctx.throw(404) + } + + ctx.body = { + user + } + } catch (err) { + if (err === 404 || err.name === 'CastError') { + ctx.throw(404) + } + + ctx.throw(500) + } + + next() +} /** * @api {put} /users/:id Update a user @@ -188,30 +178,17 @@ router.post('/', * * @apiUse TokenError */ -router.put('/:id', - ensureUser, - async (ctx) => { - try { - const user = await User.findById(ctx.params.id, '-password -salt') - if (!user) { - ctx.throw(404) - } +export async function updateUser(ctx) { + const user = ctx.body.user - Object.assign(user, ctx.request.body.user) + Object.assign(user, ctx.request.body.user) - await user.save() - ctx.body = { - user - } - } catch (err) { - if (err === 404 || err.name === 'CastError') { - ctx.throw(404) - } + await user.save() - ctx.throw(500) - } + ctx.body = { + user } -) +} /** * @api {delete} /users/:id Delete a user @@ -228,31 +205,19 @@ router.put('/:id', * @apiSuccessExample {json} Success-Response: * HTTP/1.1 200 OK * { - * "status": 200 + * "success": true * } * * @apiUse TokenError */ -router.delete('/:id', - ensureUser, - async (ctx) => { - try { - const user = await User.findById(ctx.params.id) - if (!user) { - ctx.throw(404) - } - await user.remove() +export async function deleteUser(ctx) { + const user = ctx.body.user - ctx.body = 200 - } catch (err) { - if (err === 404 || err.name === 'CastError') { - ctx.throw(404) - } + await user.remove() - ctx.throw(500) - } + ctx.status = 200 + ctx.body = { + success: true } -) - -export default router +} diff --git a/src/modules/users/router.js b/src/modules/users/router.js new file mode 100644 index 0000000..f03aed9 --- /dev/null +++ b/src/modules/users/router.js @@ -0,0 +1,48 @@ +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 + ] + } +] diff --git a/test/auth.spec.js b/test/auth.spec.js new file mode 100644 index 0000000..fa9e5f8 --- /dev/null +++ b/test/auth.spec.js @@ -0,0 +1,51 @@ +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 + expect(res.body.user.salt).to.not.exist + + context.user = res.body.user + context.token = res.body.token + + done() + }) + }) + }) +}) diff --git a/test/users.spec.js b/test/users.spec.js index 0fe356f..99931ce 100644 --- a/test/users.spec.js +++ b/test/users.spec.js @@ -56,7 +56,15 @@ describe('Users', () => { request .get(`/users?token=${token}`) .set('Accept', 'application/json') - .expect(200, done) + .expect(200, (err, res) => { + if (err) { return done(err) } + + res.body.should.have.property('users') + + res.body.users.should.have.length(1) + + done() + }) }) }) @@ -85,7 +93,16 @@ describe('Users', () => { request .get(`/users/${_id}?token=${token}`) .set('Accept', 'application/json') - .expect(200, done) + .expect(200, (err, res) => { + if (err) { return done(err) } + + res.body.should.have.property('user') + + expect(res.body.user.password).to.not.exist + expect(res.body.user.salt).to.not.exist + + done() + }) }) }) diff --git a/test/utils.js b/test/utils.js index 3ac5f12..f02445c 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,7 +1,24 @@ import mongoose from 'mongoose' -export function cleanDb () { +export function cleanDb() { for (const collection in mongoose.connection.collections) { - mongoose.connection.collections[collection].remove(); + 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 + }) + }) +}