├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── auth.js ├── clusters.js ├── db.js ├── index.js ├── libs ├── boot.js ├── config.development.js ├── config.js ├── config.test.js ├── logger.js └── middlewares.js ├── models ├── tasks.js └── users.js ├── ntask.cert ├── ntask.key ├── package.json ├── routes ├── index.js ├── tasks.js ├── token.js └── users.js └── test ├── .eslintrc ├── helpers.js ├── mocha.opts └── routes ├── index.js ├── tasks.js ├── token.js └── users.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | public/apidoc/** 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser" : "babel-eslint", 4 | "extends" : [ 5 | "airbnb/base" 6 | ], 7 | "plugins" : [ 8 | "flow-vars" 9 | ], 10 | "env" : { 11 | "node": true 12 | }, 13 | "rules": { 14 | "semi" : [2, "always"], 15 | "no-param-reassign": [2, {"props": false}], 16 | "func-names": 0, 17 | "no-console": 0, 18 | "one-var": 0, 19 | "new-cap": 0, 20 | "quote-props": 0, 21 | "prefer-template": 0, 22 | "arrow-body-style": 0, 23 | "no-empty-label": 0, 24 | "no-labels": 2, 25 | "no-unused-vars": [2, { "args": "none" }] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | ntask.sqlite 4 | public 5 | logs 6 | 7 | # osx 8 | .DS_Store 9 | 10 | # emacs 11 | *~ 12 | \#* 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-express-sequelize-es2015 2 | 3 | A boilerplate using NodeJs, Express, Sequelize, Apidoc, Eslint, Mocha, Cluster and the best practices. 4 | 5 | ## Getting up and running 6 | 7 | 1. Clone the repository 8 | 2. `npm install` 9 | 3. `npm run dev` 10 | 4. Visit `https://localhost:3000` 11 | 5. Visit `https://localhost:3000/apidoc` to see the existing API 12 | 13 | ## Some command 14 | 15 | ``` 16 | npm start # Run the serveur in cluster mode (it also generate the apidoc) 17 | npm lint # Check lint error using eslint 18 | npm test # run mocha tests 19 | npm apidoc # Generate the apidoc 20 | ``` 21 | 22 | ## Coding Style 23 | 24 | The coding style used is the airbnb one: https://github.com/airbnb/javascript 25 | 26 | You can configure it in the `.eslintrc` file. 27 | 28 | ## Architecture 29 | 30 | The architecture is MVR (Model View Route) 31 | 32 | The files are structured in the following manner: 33 | ``` 34 | libs (Configurations files and libs) 35 | models (Sequelize models) 36 | routes (Express routes, the comments in this folder are used to generate the apidoc) 37 | test (mocha test, 'npm test' to run it) 38 | auth.js (Configuration for JWT auth (JSON Web Tokens)) 39 | cluster.js (Used to run in cluster mode. We could use https://github.com/Unitech/pm2 too) 40 | index.js (Entry point) 41 | ntask.* (SSL certificates) 42 | ``` 43 | 44 | If you want to have a better understanding of the architecture, I recommend you to read [Caio Ribeiro Pereira's book](https://leanpub.com/building-apis-with-nodejs). 45 | 46 | ## Credit 47 | 48 | This project is base on [the book of Caio Ribeiro Pereira](https://leanpub.com/building-apis-with-nodejs) and [his repository](https://github.com/caio-ribeiro-pereira/building-apis-with-nodejs) so, all the credit goes to him. Also, I recommend you his book. 49 | -------------------------------------------------------------------------------- /auth.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import { Strategy, ExtractJwt } from 'passport-jwt'; 3 | 4 | module.exports = app => { 5 | const Users = app.db.models.Users; 6 | const cfg = app.libs.config; 7 | const params = { 8 | secretOrKey: cfg.jwtSecret, 9 | jwtFromRequest: ExtractJwt.fromAuthHeader(), 10 | }; 11 | const strategy = new Strategy(params, (payload, done) => { 12 | Users.findById(payload.id) 13 | .then(user => { 14 | if (user) { 15 | return done(null, { 16 | id: user.id, 17 | email: user.email, 18 | }); 19 | } 20 | return done(null, false); 21 | }) 22 | .catch(error => done(error, null)); 23 | }); 24 | passport.use(strategy); 25 | return { 26 | initialize: () => { 27 | return passport.initialize(); 28 | }, 29 | authenticate: () => { 30 | return passport.authenticate('jwt', cfg.jwtSession); 31 | }, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /clusters.js: -------------------------------------------------------------------------------- 1 | import cluster from 'cluster'; 2 | import os from 'os'; 3 | 4 | const CPUS = os.cpus(); 5 | if (cluster.isMaster) { 6 | CPUS.forEach(() => cluster.fork()); 7 | cluster.on('listening', worker => { 8 | console.log('Cluster %d connected', worker.process.pid); 9 | }); 10 | cluster.on('disconnect', worker => { 11 | console.log('Cluster %d disconnected', worker.process.pid); 12 | }); 13 | cluster.on('exit', worker => { 14 | console.log('Cluster %d is dead', worker.process.pid); 15 | cluster.fork(); 16 | // Ensure to starts a new cluster if an old one dies 17 | }); 18 | } else { 19 | require('./index.js'); 20 | } 21 | -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import Sequelize from 'sequelize'; 4 | 5 | let db = null; 6 | 7 | module.exports = app => { 8 | if (!db) { 9 | const config = app.libs.config; 10 | const sequelize = new Sequelize( 11 | config.database, 12 | config.username, 13 | config.password, 14 | config.params 15 | ); 16 | db = { 17 | sequelize, 18 | Sequelize, 19 | models: {}, 20 | }; 21 | const dir = path.join(__dirname, 'models'); 22 | fs.readdirSync(dir).forEach(file => { 23 | const modelDir = path.join(dir, file); 24 | const model = sequelize.import(modelDir); 25 | db.models[model.name] = model; 26 | }); 27 | Object.keys(db.models).forEach(key => { 28 | db.models[key].associate(db.models); 29 | }); 30 | } 31 | return db; 32 | }; 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import consign from 'consign'; 3 | 4 | const app = express(); 5 | 6 | consign({ verbose: false }) 7 | .include('libs/config.js') 8 | .then('db.js') 9 | .then('auth.js') 10 | .then('libs/middlewares.js') 11 | .then('routes') 12 | .then('libs/boot.js') 13 | .into(app); 14 | 15 | module.exports = app; 16 | -------------------------------------------------------------------------------- /libs/boot.js: -------------------------------------------------------------------------------- 1 | import https from 'https'; 2 | import fs from 'fs'; 3 | 4 | module.exports = app => { 5 | if (process.env.NODE_ENV !== 'test') { 6 | const credentials = { 7 | key: fs.readFileSync('ntask.key', 'utf8'), 8 | cert: fs.readFileSync('ntask.cert', 'utf8'), 9 | }; 10 | app.db.sequelize.sync().done(() => { 11 | https.createServer(credentials, app) 12 | .listen(app.get('port'), () => { 13 | console.log(`NTask API - Port ${app.get('port')}`); 14 | }); 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /libs/config.development.js: -------------------------------------------------------------------------------- 1 | import logger from './logger.js'; 2 | 3 | module.exports = { 4 | database: 'ntask', 5 | username: '', 6 | password: '', 7 | params: { 8 | dialect: 'sqlite', 9 | storage: 'ntask.sqlite', 10 | logging: (sql) => { 11 | logger.info(`[${new Date()}] ${sql}`); 12 | }, 13 | define: { 14 | underscored: true, 15 | }, 16 | }, 17 | jwtSecret: 'Nta$K-AP1', 18 | jwtSession: { session: false }, 19 | }; 20 | -------------------------------------------------------------------------------- /libs/config.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const env = process.env.NODE_ENV; 3 | if (env) { 4 | return require(`./config.${env}.js`); 5 | } 6 | return require('./config.development.js'); 7 | }; 8 | -------------------------------------------------------------------------------- /libs/config.test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | database: 'ntask_test', 3 | username: '', 4 | password: '', 5 | params: { 6 | dialect: 'sqlite', 7 | storage: 'ntask.sqlite', 8 | logging: false, 9 | define: { 10 | underscored: true, 11 | }, 12 | }, 13 | jwtSecret: 'NTASK_TEST', 14 | jwtSession: { session: false }, 15 | }; 16 | -------------------------------------------------------------------------------- /libs/logger.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import winston from 'winston'; 3 | 4 | if (!fs.existsSync('logs')) { 5 | fs.mkdirSync('logs'); 6 | } 7 | 8 | module.exports = new winston.Logger({ 9 | transports: [ 10 | new winston.transports.File({ 11 | level: 'info', 12 | filename: 'logs/app.log', 13 | maxsize: 1048576, 14 | maxFiles: 10, 15 | colorize: false, 16 | }), 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /libs/middlewares.js: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser'; 2 | import express from 'express'; 3 | import morgan from 'morgan'; 4 | import cors from 'cors'; 5 | import helmet from 'helmet'; 6 | import logger from './logger.js'; 7 | 8 | module.exports = app => { 9 | app.set('port', 3000); 10 | app.set('json spaces', 4); 11 | app.use(morgan('common', { 12 | stream: { 13 | write: (message) => { 14 | logger.info(message); 15 | }, 16 | }, 17 | })); 18 | app.use(helmet()); 19 | app.use(cors({ 20 | origin: ['http://localhost:3001'], 21 | methods: ['GET', 'POST', 'PUT', 'DELETE'], 22 | allowedHeaders: ['Content-Type', 'Authorization'], 23 | })); 24 | app.use(bodyParser.json()); 25 | app.use(app.auth.initialize()); 26 | app.use((req, res, next) => { 27 | delete req.body.id; 28 | next(); 29 | }); 30 | app.use(express.static('public')); 31 | }; 32 | -------------------------------------------------------------------------------- /models/tasks.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataType) => { 2 | const Tasks = sequelize.define('Tasks', { 3 | id: { 4 | type: DataType.INTEGER, 5 | primaryKey: true, 6 | autoIncrement: true, 7 | }, 8 | title: { 9 | type: DataType.STRING, 10 | allowNull: false, 11 | validate: { 12 | notEmpty: true, 13 | }, 14 | }, 15 | done: { 16 | type: DataType.BOOLEAN, 17 | allowNull: false, 18 | defaultValue: false, 19 | }, 20 | }, { 21 | classMethods: { 22 | associate: (models) => { 23 | Tasks.belongsTo(models.Users); 24 | }, 25 | }, 26 | }); 27 | return Tasks; 28 | }; 29 | -------------------------------------------------------------------------------- /models/users.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | 3 | module.exports = (sequelize, DataType) => { 4 | const Users = sequelize.define('Users', { 5 | id: { 6 | type: DataType.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true, 9 | }, 10 | name: { 11 | type: DataType.STRING, 12 | allowNull: false, 13 | validate: { 14 | notEmpty: true, 15 | }, 16 | }, 17 | password: { 18 | type: DataType.STRING, 19 | allowNull: false, 20 | validate: { 21 | notEmpty: true, 22 | }, 23 | }, 24 | email: { 25 | type: DataType.STRING, 26 | unique: true, 27 | allowNull: false, 28 | validate: { 29 | notEmpty: true, 30 | }, 31 | }, 32 | }, { 33 | hooks: { 34 | beforeCreate: user => { 35 | const salt = bcrypt.genSaltSync(); 36 | user.password = bcrypt.hashSync(user.password, salt); 37 | }, 38 | }, 39 | classMethods: { 40 | associate: models => { 41 | Users.hasMany(models.Tasks); 42 | }, 43 | isPassword: (encodedPassword, password) => { 44 | return bcrypt.compareSync(password, encodedPassword); 45 | }, 46 | }, 47 | }); 48 | return Users; 49 | }; 50 | -------------------------------------------------------------------------------- /ntask.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC8zCCAdugAwIBAgIJAMltM6AKqPQxMA0GCSqGSIb3DQEBBQUAMBAxDjAMBgNV 3 | BAMMBW50YXNrMB4XDTE1MTAwNDE2MzIzNVoXDTI1MTAwMTE2MzIzNVowEDEOMAwG 4 | A1UEAwwFbnRhc2swggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJ6P38 5 | AGoIpHpgOGdEYfh0/Tf5OL0XPFh2PK+wWsCCZzgmL5RUnfV+Gw3wR/rwfHeOJSXy 6 | cSwq7UJe0Rf0EnMULBpp+oeb2iGqbcAke7DTOzaihcp27l+zzNK/BvXq/BtI7Plm 7 | 4mziIuCf3QOeQemkPEiztGZKEJeEE/AFSZmx4J1ugJ0G+fqjkMWTXcYj0RWlqG2y 8 | LBmDFErQANaUeYOrjdSIp96MBFLC8afYnNp/XrItm1XqQ29mUpbwRw2qnR+WdM6S 9 | YZiNcEo/eXLDBnLYiJjSqoiY5hd6CiCO3yMv4IT/LdR1V+FTDg5KecQ7WD5/IKIA 10 | 44844xZzVGkgt3xPAgMBAAGjUDBOMB0GA1UdDgQWBBTAx3g/qsJRyaYpDS6AGTaT 11 | f43bLTAfBgNVHSMEGDAWgBTAx3g/qsJRyaYpDS6AGTaTf43bLTAMBgNVHRMEBTAD 12 | AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQC2Pwv5W/DQ2OFmHsGPVRwGBY0UP0PuTxpX 13 | 4rOe9ZtJDi2QRybA59iEdTT3pe7FX+LPd0DUiJswwYF8+BYDVjJRHiIdcFDC7Jqb 14 | nk3g1JdxzzMdhPpexHkkYzU8Zaxl1wkp3KDlrfqRD2dYSzaToiR8UjkDAwjXcfmA 15 | 489HPKBtZLWydbV/3J9EW/I6zmKgampdIn0qOp6FlCN2dgfarpD8bUxruLgN2wFu 16 | Bb8g3hzQo5ZC7Ws98TIh0B8MFidzMuLjqr1azm147wUr/NfnXS1a4w7/r302sGwa 17 | OuNmGHImnUpAnVpjV482M/DdUy5VYP0M45ImzX+rbJOUffFobx6E 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /ntask.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAyej9/ABqCKR6YDhnRGH4dP03+Ti9FzxYdjyvsFrAgmc4Ji+U 3 | VJ31fhsN8Ef68Hx3jiUl8nEsKu1CXtEX9BJzFCwaafqHm9ohqm3AJHuw0zs2ooXK 4 | du5fs8zSvwb16vwbSOz5ZuJs4iLgn90DnkHppDxIs7RmShCXhBPwBUmZseCdboCd 5 | Bvn6o5DFk13GI9EVpahtsiwZgxRK0ADWlHmDq43UiKfejARSwvGn2Jzaf16yLZtV 6 | 6kNvZlKW8EcNqp0flnTOkmGYjXBKP3lywwZy2IiY0qqImOYXegogjt8jL+CE/y3U 7 | dVfhUw4OSnnEO1g+fyCiAOOPOOMWc1RpILd8TwIDAQABAoIBAHKTuv10Jre8zo0n 8 | tMJDbkDFKSxOHE/BONnv2isTdMcLV/ujaGMUOClVpPVDg41QtG9/eSc5Pb0mYlF4 9 | CkXA6nj6Bgs51haFFDGoki6h2lgj8/8KOTiAUOKxSq6IfqjYY4tgnq7Zsrwo2psd 10 | Sl5WPQWsB/2iU6GYBMM4pS369DLRp5RIu7fvGv4gT9hWxQldNCg0oDadsm185FBL 11 | 0TlKDd2fFnt0Be96yDpwli/Kd2xh3x58/J3AZNrC3dQl8LLPgoo+YvPSzz3Y/Ilj 12 | HImngbQY/c2k6F8wun8H1x0MhBFrN52Q8F0YSLMd3EDR9aaZzGUJfsbSjMsklSIz 13 | Q74mHCECgYEA622fv37f3+V2Uz/H8Tb/r/ym5kHtVxzxszQ84gpzNQjD8IZcfIbu 14 | AldJocHpKuWeN4W5Ttvi5WyH4o77FRz2OBHvmdTmWkrbISH0x7JKq4442PAYamD1 15 | iFCJrLBYVK+6mycBILRqm32eJARrTuBN5DHgvEiXg0DkX9GAxZrhSJcCgYEA242X 16 | FlSNV8OLzjCubJU+Vv0MijX7tMnzmziWCAabxOJUyWEsk4QjoQTL4gQjqNXIDfKt 17 | 162n7mKk7/GJZOG9gC7lZw7AIGwTQ9WyjfvgQIbFo2V9zDF2KGDX5oeW8qPISi52 18 | l0TocSKxy5T+7QQJMH+8Be7OSVvIlEAMu0LqaQkCgYAdPq/ibNNIj8uECd8/cpKO 19 | fPcKkVP3R0wq86lAdwXap60XWslwWp6EQe2On3TkdEOUKBNd3WixEStMFHDSLZfU 20 | XT4DQPQgcT4JPpuWluo5p2AeaqzNwh+eAEsp3XoLgwzOKykzs9WuXQtg8/+Ue76R 21 | QzTkjqvrjQsRcAfsBBJKHwKBgHKhDVZKVPWSkibYQelNTpwKSIbMwptUqYzMUYDl 22 | OmTkKpJt2uE2J4gFQhHCSX/4BhhKMTufXkNXW3gvaqWyOsd3NKzHBcanxrMvGqeI 23 | 7z+hXgT+k1yOInvYfEDPYB9VJdidQ6uc/aM8EwoQw7yp08ZvmpKaaTfh5OqKOlt3 24 | B35JAoGBALZ78bt2PJKdQyxO8WtSBUsD8lrB91qyNJRZhM2fMnAVe9eKAeOXtyD5 25 | DRsSp65CUMzBXN7f0JYutumBh5LSUX1RYzNtwcL4/Rj8mXMssYBE3TQ3Ajc8nAES 26 | W3uk7jU0ZNjRsAbpgaiGEH1uGXH7euC+XMpfDILAptxsam8r+ODA 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-express-sequelize-es2015", 3 | "version": "1.0.0", 4 | "description": "API server boilerplate using Node/Express/Sequelize", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run apidoc && npm run clusters", 8 | "clusters": "babel-node clusters.js", 9 | "dev": "better-npm-run dev", 10 | "lint": "eslint .", 11 | "lint:fix": "npm run lint -- --fix", 12 | "test": "NODE_ENV=test mocha test/**/*.js", 13 | "apidoc": "apidoc -i routes/ -o public/apidoc", 14 | "deploy": "better-npm-run deploy" 15 | }, 16 | "apidoc": { 17 | "name": "Node Task API - Documentation", 18 | "template": { 19 | "forceLanguage": "en" 20 | } 21 | }, 22 | "betterScripts": { 23 | "dev": { 24 | "command": "nodemon --exec babel-node index.js", 25 | "env": { 26 | "NODE_ENV": "development", 27 | "DEBUG": "app:*" 28 | } 29 | }, 30 | "deploy": { 31 | "command": "echo TODO", 32 | "env": { 33 | "NODE_ENV": "production", 34 | "DEBUG": "app:*" 35 | } 36 | } 37 | }, 38 | "author": "Arnaud Valensi", 39 | "dependencies": { 40 | "bcrypt": "^0.8.5", 41 | "body-parser": "^1.15.0", 42 | "compression": "^1.6.1", 43 | "consign": "^0.1.2", 44 | "cors": "^2.7.1", 45 | "express": "^4.13.4", 46 | "helmet": "^1.1.0", 47 | "jwt-simple": "^0.4.1", 48 | "morgan": "^1.6.1", 49 | "passport": "^0.3.2", 50 | "passport-jwt": "^2.0.0", 51 | "sequelize": "^3.19.2", 52 | "sqlite3": "^3.1.1", 53 | "winston": "^2.1.1" 54 | }, 55 | "devDependencies": { 56 | "apidoc": "^0.15.1", 57 | "babel-register": "^6.5.2", 58 | "babel-cli": "^6.5.1", 59 | "babel-eslint": "^5.0.0", 60 | "babel-preset-es2015": "^6.5.0", 61 | "babel-preset-stage-0": "^6.5.0", 62 | "eslint": "^1.10.3", 63 | "eslint-config-airbnb": "^5.0.1", 64 | "eslint-plugin-flow-vars": "^0.1.3", 65 | "chai": "^3.5.0", 66 | "mocha": "^2.4.5", 67 | "supertest": "^1.2.0", 68 | "better-npm-run": "0.0.5", 69 | "nodemon": "^1.8.1" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | /** 3 | * @api {get} / API Status 4 | * @apiGroup Status 5 | * @apiSuccess {String} status API Status' message 6 | * @apiSuccessExample {json} Success 7 | * HTTP/1.1 200 OK 8 | * {"status": "NTask API"} 9 | */ 10 | app.get('/', (req, res) => { 11 | res.json({ status: 'NTask API' }); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /routes/tasks.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const Tasks = app.db.models.Tasks; 3 | 4 | app.route('/tasks') 5 | .all(app.auth.authenticate()) 6 | /** 7 | * @api {get} /tasks List the user's tasks 8 | * @apiGroup Tasks 9 | * @apiHeader {String} Authorization Token of authenticated user 10 | * @apiHeaderExample {json} Header 11 | * {"Authorization": "JWT xyz.abc.123.hgf"} 12 | * @apiSuccess {Object[]} tasks Task's list 13 | * @apiSuccess {Number} tasks.id Task id 14 | * @apiSuccess {String} tasks.title Task title 15 | * @apiSuccess {Boolean} tasks.done Task is done? 16 | * @apiSuccess {Date} tasks.updated_at Update's date 17 | * @apiSuccess {Date} tasks.created_at Register's date 18 | * @apiSuccess {Number} tasks.user_id Id do usuário 19 | * @apiSuccessExample {json} Success 20 | * HTTP/1.1 200 OK 21 | * [{ 22 | * "id": 1, 23 | * "title": "Study", 24 | * "done": false 25 | * "updated_at": "2016-02-10T15:46:51.778Z", 26 | * "created_at": "2016-02-10T15:46:51.778Z", 27 | * "user_id": 1 28 | * }] 29 | * @apiErrorExample {json} List error 30 | * HTTP/1.1 412 Precondition Failed 31 | */ 32 | .get((req, res) => { 33 | Tasks.findAll({ 34 | where: { user_id: req.user.id }, 35 | }) 36 | .then(result => res.json(result)) 37 | .catch(error => { 38 | res.status(412).json({ msg: error.message }); 39 | }); 40 | }) 41 | /** 42 | * @api {post} /tasks Register a new task 43 | * @apiGroup Tasks 44 | * @apiHeader {String} Authorization Token of authenticated user 45 | * @apiHeaderExample {json} Header 46 | * {"Authorization": "JWT xyz.abc.123.hgf"} 47 | * @apiParam {String} title Task title 48 | * @apiParamExample {json} Input 49 | * {"title": "Study"} 50 | * @apiSuccess {Number} id Task id 51 | * @apiSuccess {String} title Task title 52 | * @apiSuccess {Boolean} done=false Task is done? 53 | * @apiSuccess {Date} updated_at Update's date 54 | * @apiSuccess {Date} created_at Register's date 55 | * @apiSuccess {Number} user_id User id 56 | * @apiSuccessExample {json} Success 57 | * HTTP/1.1 200 OK 58 | * { 59 | * "id": 1, 60 | * "title": "Study", 61 | * "done": false, 62 | * "updated_at": "2016-02-10T15:46:51.778Z", 63 | * "created_at": "2016-02-10T15:46:51.778Z", 64 | * "user_id": 1 65 | * } 66 | * @apiErrorExample {json} Register error 67 | * HTTP/1.1 412 Precondition Failed 68 | */ 69 | .post((req, res) => { 70 | req.body.user_id = req.user.id; 71 | Tasks.create(req.body) 72 | .then(result => res.json(result)) 73 | .catch(error => { 74 | res.status(412).json({ msg: error.message }); 75 | }); 76 | }); 77 | 78 | app.route('/tasks/:id') 79 | .all(app.auth.authenticate()) 80 | /** 81 | * @api {get} /tasks/:id Get a task 82 | * @apiGroup Tasks 83 | * @apiHeader {String} Authorization Token of authenticated user 84 | * @apiHeaderExample {json} Header 85 | * {"Authorization": "JWT xyz.abc.123.hgf"} 86 | * @apiParam {id} id Task id 87 | * @apiSuccess {Number} id Task id 88 | * @apiSuccess {String} title Task title 89 | * @apiSuccess {Boolean} done Task is done? 90 | * @apiSuccess {Date} updated_at Update's date 91 | * @apiSuccess {Date} created_at Register's date 92 | * @apiSuccess {Number} user_id User id 93 | * @apiSuccessExample {json} Success 94 | * HTTP/1.1 200 OK 95 | * { 96 | * "id": 1, 97 | * "title": "Study", 98 | * "done": false 99 | * "updated_at": "2016-02-10T15:46:51.778Z", 100 | * "created_at": "2016-02-10T15:46:51.778Z", 101 | * "user_id": 1 102 | * } 103 | * @apiErrorExample {json} Task not found error 104 | * HTTP/1.1 404 Not Found 105 | * @apiErrorExample {json} Find error 106 | * HTTP/1.1 412 Precondition Failed 107 | */ 108 | .get((req, res) => { 109 | Tasks.findOne({ where: { 110 | id: req.params.id, 111 | user_id: req.user.id, 112 | } }) 113 | .then(result => { 114 | if (result) { 115 | res.json(result); 116 | } else { 117 | res.sendStatus(404); 118 | } 119 | }) 120 | .catch(error => { 121 | res.status(412).json({ msg: error.message }); 122 | }); 123 | }) 124 | /** 125 | * @api {put} /tasks/:id Update a task 126 | * @apiGroup Tasks 127 | * @apiHeader {String} Authorization Token of authenticated user 128 | * @apiHeaderExample {json} Header 129 | * {"Authorization": "JWT xyz.abc.123.hgf"} 130 | * @apiParam {id} id Task id 131 | * @apiParam {String} title Task title 132 | * @apiParam {Boolean} done Task is done? 133 | * @apiParamExample {json} Input 134 | * { 135 | * "title": "Work", 136 | * "done": true 137 | * } 138 | * @apiSuccessExample {json} Success 139 | * HTTP/1.1 204 No Content 140 | * @apiErrorExample {json} Update error 141 | * HTTP/1.1 412 Precondition Failed 142 | */ 143 | .put((req, res) => { 144 | Tasks.update(req.body, { where: { 145 | id: req.params.id, 146 | user_id: req.user.id, 147 | } }) 148 | .then(result => res.sendStatus(204)) 149 | .catch(error => { 150 | res.status(412).json({ msg: error.message }); 151 | }); 152 | }) 153 | /** 154 | * @api {delete} /tasks/:id Remove a task 155 | * @apiGroup Tasks 156 | * @apiHeader {String} Authorization Token of authenticated user 157 | * @apiHeaderExample {json} Header 158 | * {"Authorization": "JWT xyz.abc.123.hgf"} 159 | * @apiParam {id} id Task id 160 | * @apiSuccessExample {json} Success 161 | * HTTP/1.1 204 No Content 162 | * @apiErrorExample {json} Delete error 163 | * HTTP/1.1 412 Precondition Failed 164 | */ 165 | .delete((req, res) => { 166 | Tasks.destroy({ where: { 167 | id: req.params.id, 168 | user_id: req.user.id, 169 | } }) 170 | .then(result => res.sendStatus(204)) 171 | .catch(error => { 172 | res.status(412).json({ msg: error.message }); 173 | }); 174 | }); 175 | }; 176 | -------------------------------------------------------------------------------- /routes/token.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jwt-simple'; 2 | 3 | module.exports = app => { 4 | const cfg = app.libs.config; 5 | const Users = app.db.models.Users; 6 | 7 | /** 8 | * @api {post} /token Authentication Token 9 | * @apiGroup Credentials 10 | * @apiParam {String} email User email 11 | * @apiParam {String} password User password 12 | * @apiParamExample {json} Input 13 | * { 14 | * "email": "john@connor.net", 15 | * "password": "123456" 16 | * } 17 | * @apiSuccess {String} token Token of authenticated user 18 | * @apiSuccessExample {json} Success 19 | * HTTP/1.1 200 OK 20 | * {"token": "xyz.abc.123.hgf"} 21 | * @apiErrorExample {json} Authentication error 22 | * HTTP/1.1 401 Unauthorized 23 | */ 24 | app.post('/token', (req, res) => { 25 | if (req.body.email && req.body.password) { 26 | const email = req.body.email; 27 | const password = req.body.password; 28 | Users.findOne({ where: { email } }) 29 | .then(user => { 30 | if (Users.isPassword(user.password, password)) { 31 | const payload = { id: user.id }; 32 | res.json({ 33 | token: jwt.encode(payload, cfg.jwtSecret), 34 | }); 35 | } else { 36 | res.sendStatus(401); 37 | } 38 | }) 39 | .catch(error => res.sendStatus(401)); 40 | } else { 41 | res.sendStatus(401); 42 | } 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const Users = app.db.models.Users; 3 | 4 | app.route('/user') 5 | .all(app.auth.authenticate()) 6 | /** 7 | * @api {get} /user Return the authenticated user's data 8 | * @apiGroup User 9 | * @apiHeader {String} Authorization Token of authenticated user 10 | * @apiHeaderExample {json} Header 11 | * {"Authorization": "JWT xyz.abc.123.hgf"} 12 | * @apiSuccess {Number} id User id 13 | * @apiSuccess {String} name User name 14 | * @apiSuccess {String} email User email 15 | * @apiSuccessExample {json} Success 16 | * HTTP/1.1 200 OK 17 | * { 18 | * "id": 1, 19 | * "name": "John Connor", 20 | * "email": "john@connor.net" 21 | * } 22 | * @apiErrorExample {json} Find error 23 | * HTTP/1.1 412 Precondition Failed 24 | */ 25 | .get((req, res) => { 26 | Users.findById(req.user.id, { 27 | attributes: ['id', 'name', 'email'], 28 | }) 29 | .then(result => res.json(result)) 30 | .catch(error => { 31 | res.status(412).json({ msg: error.message }); 32 | }); 33 | }) 34 | /** 35 | * @api {delete} /user Deletes an authenticated user 36 | * @apiGroup User 37 | * @apiHeader {String} Authorization Token of authenticated user 38 | * @apiHeaderExample {json} Header 39 | * {"Authorization": "JWT xyz.abc.123.hgf"} 40 | * @apiSuccessExample {json} Success 41 | * HTTP/1.1 204 No Content 42 | * @apiErrorExample {json} Delete error 43 | * HTTP/1.1 412 Precondition Failed 44 | */ 45 | .delete((req, res) => { 46 | Users.destroy({ where: { id: req.user.id } }) 47 | .then(result => res.sendStatus(204)) 48 | .catch(error => { 49 | res.status(412).json({ msg: error.message }); 50 | }); 51 | }); 52 | 53 | /** 54 | * @api {post} /users Register a new user 55 | * @apiGroup User 56 | * @apiParam {String} name User name 57 | * @apiParam {String} email User email 58 | * @apiParam {String} password User password 59 | * @apiParamExample {json} Input 60 | * { 61 | * "name": "John Connor", 62 | * "email": "john@connor.net", 63 | * "password": "123456" 64 | * } 65 | * @apiSuccess {Number} id User id 66 | * @apiSuccess {String} name User name 67 | * @apiSuccess {String} email User email 68 | * @apiSuccess {String} password User encrypted password 69 | * @apiSuccess {Date} updated_at Update's date 70 | * @apiSuccess {Date} created_at Register's date 71 | * @apiSuccessExample {json} Success 72 | * HTTP/1.1 200 OK 73 | * { 74 | * "id": 1, 75 | * "name": "John Connor", 76 | * "email": "john@connor.net", 77 | * "password": "$2a$10$SK1B1", 78 | * "updated_at": "2016-02-10T15:20:11.700Z", 79 | * "created_at": "2016-02-10T15:29:11.700Z", 80 | * } 81 | * @apiErrorExample {json} Register error 82 | * HTTP/1.1 412 Precondition Failed 83 | */ 84 | app.post('/users', (req, res) => { 85 | Users.create(req.body) 86 | .then(result => res.json(result)) 87 | .catch(error => { 88 | res.status(412).json({ msg: error.message }); 89 | }); 90 | }); 91 | }; 92 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "describe": true, 4 | "it": true, 5 | "expect": true, 6 | "beforeEach": true, 7 | "app": true, 8 | "request": true 9 | }, 10 | "rules": { 11 | "no-unused-expressions": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import supertest from 'supertest'; 2 | import chai from 'chai'; 3 | import app from '../index.js'; 4 | 5 | global.app = app; 6 | global.request = supertest(app); 7 | global.expect = chai.expect; 8 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require test/helpers 2 | --reporter spec 3 | --compilers js:babel-register 4 | --slow 5000 5 | -------------------------------------------------------------------------------- /test/routes/index.js: -------------------------------------------------------------------------------- 1 | describe('Routes: Index', () => { 2 | describe('GET /', () => { 3 | it('returns the API status', done => { 4 | request.get('/') 5 | .expect(200) 6 | .end((err, res) => { 7 | const expected = { status: 'NTask API' }; 8 | expect(res.body).to.eql(expected); 9 | done(err); 10 | }); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/routes/tasks.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jwt-simple'; 2 | 3 | describe('Routes: Tasks', () => { 4 | const Users = app.db.models.Users; 5 | const Tasks = app.db.models.Tasks; 6 | const jwtSecret = app.libs.config.jwtSecret; 7 | let token; 8 | let fakeTask; 9 | beforeEach(done => { 10 | Users 11 | .destroy({ where: {} }) 12 | .then(() => Users.create({ 13 | name: 'John', 14 | email: 'john@mail.net', 15 | password: '12345', 16 | })) 17 | .then(user => { 18 | Tasks 19 | .destroy({ where: {} }) 20 | .then(() => Tasks.bulkCreate([{ 21 | id: 1, 22 | title: 'Work', 23 | user_id: user.id, 24 | }, { 25 | id: 2, 26 | title: 'Study', 27 | user_id: user.id, 28 | }])) 29 | .then(tasks => { 30 | fakeTask = tasks[0]; 31 | token = jwt.encode({ id: user.id }, jwtSecret); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | describe('GET /tasks', () => { 37 | describe('status 200', () => { 38 | it('returns a list of tasks', done => { 39 | request.get('/tasks') 40 | .set('Authorization', `JWT ${token}`) 41 | .expect(200) 42 | .end((err, res) => { 43 | expect(res.body).to.have.length(2); 44 | expect(res.body[0].title).to.eql('Work'); 45 | expect(res.body[1].title).to.eql('Study'); 46 | done(err); 47 | }); 48 | }); 49 | }); 50 | }); 51 | describe('POST /tasks', () => { 52 | describe('status 200', () => { 53 | it('creates a new task', done => { 54 | request.post('/tasks') 55 | .set('Authorization', `JWT ${token}`) 56 | .send({ title: 'Run' }) 57 | .expect(200) 58 | .end((err, res) => { 59 | expect(res.body.title).to.eql('Run'); 60 | expect(res.body.done).to.be.false; 61 | done(err); 62 | }); 63 | }); 64 | }); 65 | }); 66 | describe('GET /tasks/:id', () => { 67 | describe('status 200', () => { 68 | it('returns one task', done => { 69 | request.get(`/tasks/${fakeTask.id}`) 70 | .set('Authorization', `JWT ${token}`) 71 | .expect(200) 72 | .end((err, res) => { 73 | expect(res.body.title).to.eql('Work'); 74 | done(err); 75 | }); 76 | }); 77 | }); 78 | describe('status 404', () => { 79 | it('throws error when task not exist', done => { 80 | request.get('/tasks/0') 81 | .set('Authorization', `JWT ${token}`) 82 | .expect(404) 83 | .end((err, res) => done(err)); 84 | }); 85 | }); 86 | }); 87 | describe('PUT /tasks/:id', () => { 88 | describe('status 204', () => { 89 | it('updates a task', done => { 90 | request.put(`/tasks/${fakeTask.id}`) 91 | .set('Authorization', `JWT ${token}`) 92 | .send({ 93 | title: 'Travel', 94 | done: true, 95 | }) 96 | .expect(204) 97 | .end((err, res) => done(err)); 98 | }); 99 | }); 100 | }); 101 | describe('DELETE /tasks/:id', () => { 102 | describe('status 204', () => { 103 | it('removes a task', done => { 104 | request.delete(`/tasks/${fakeTask.id}`) 105 | .set('Authorization', `JWT ${token}`) 106 | .expect(204) 107 | .end((err, res) => done(err)); 108 | }); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/routes/token.js: -------------------------------------------------------------------------------- 1 | describe('Routes: Token', () => { 2 | const Users = app.db.models.Users; 3 | describe('POST /token', () => { 4 | beforeEach(done => { 5 | Users 6 | .destroy({ where: {} }) 7 | .then(() => Users.create({ 8 | name: 'John', 9 | email: 'john@mail.net', 10 | password: '12345', 11 | })) 12 | .then(done()); 13 | }); 14 | describe('status 200', () => { 15 | it('returns authenticated user token', done => { 16 | request.post('/token') 17 | .send({ 18 | email: 'john@mail.net', 19 | password: '12345', 20 | }) 21 | .expect(200) 22 | .end((err, res) => { 23 | expect(res.body).to.include.keys('token'); 24 | done(err); 25 | }); 26 | }); 27 | }); 28 | describe('status 401', () => { 29 | it('throws error when password is incorrect', done => { 30 | request.post('/token') 31 | .send({ 32 | email: 'john@mail.net', 33 | password: 'WRONG_PASSWORD', 34 | }) 35 | .expect(401) 36 | .end((err, res) => { 37 | done(err); 38 | }); 39 | }); 40 | it('throws error when email not exist', done => { 41 | request.post('/token') 42 | .send({ 43 | email: 'wrong@email.com', 44 | password: '12345', 45 | }) 46 | .expect(401) 47 | .end((err, res) => { 48 | done(err); 49 | }); 50 | }); 51 | it('throws error when email and password are blank', done => { 52 | request.post('/token') 53 | .expect(401) 54 | .end((err, res) => { 55 | done(err); 56 | }); 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/routes/users.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jwt-simple'; 2 | 3 | describe('Routes: Tasks', () => { 4 | const Users = app.db.models.Users; 5 | const jwtSecret = app.libs.config.jwtSecret; 6 | let token; 7 | beforeEach(done => { 8 | Users 9 | .destroy({ where: {} }) 10 | .then(() => Users.create({ 11 | name: 'John', 12 | email: 'john@mail.net', 13 | password: '12345', 14 | })) 15 | .then(user => { 16 | token = jwt.encode({ id: user.id }, jwtSecret); 17 | done(); 18 | }); 19 | }); 20 | describe('GET /user', () => { 21 | describe('status 200', () => { 22 | it('returns an authenticated user', done => { 23 | request.get('/user') 24 | .set('Authorization', `JWT ${token}`) 25 | .expect(200) 26 | .end((err, res) => { 27 | expect(res.body.name).to.eql('John'); 28 | expect(res.body.email).to.eql('john@mail.net'); 29 | done(err); 30 | }); 31 | }); 32 | }); 33 | }); 34 | describe('DELETE /user', () => { 35 | describe('status 204', () => { 36 | it('deletes an authenticated user', done => { 37 | request.delete('/user') 38 | .set('Authorization', `JWT ${token}`) 39 | .expect(204) 40 | .end((err, res) => done(err)); 41 | }); 42 | }); 43 | }); 44 | describe('POST /users', () => { 45 | describe('status 200', () => { 46 | it('creates a new user', done => { 47 | request.post('/users') 48 | .send({ 49 | name: 'Mary', 50 | email: 'mary@mail.net', 51 | password: '12345', 52 | }) 53 | .expect(200) 54 | .end((err, res) => { 55 | expect(res.body.name).to.eql('Mary'); 56 | expect(res.body.email).to.eql('mary@mail.net'); 57 | done(err); 58 | }); 59 | }); 60 | }); 61 | }); 62 | }); 63 | --------------------------------------------------------------------------------