├── Procfile ├── .gitignore ├── server ├── api │ ├── api.js │ └── user │ │ ├── routes.js │ │ └── controller.js ├── auth │ ├── routes.js │ ├── controller.js │ └── auth.js ├── config │ ├── config.js │ └── middlewares.js ├── server.js └── utils │ └── helpers.js ├── index.js ├── .eslintrc ├── seeders └── users.js ├── config └── config.json ├── LICENSE ├── models ├── index.js └── user.js ├── README.md ├── test ├── auth │ └── auth.test.js ├── user │ └── user.test.js └── utils │ └── helpers.test.js └── package.json /Procfile: -------------------------------------------------------------------------------- 1 | web: node web.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | NOTE.md 3 | coverage 4 | yarn.lock 5 | .travis.yml -------------------------------------------------------------------------------- /server/api/api.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | // api router will mount other routers for all our resources 4 | router.use('/users', require('./user/routes')); 5 | 6 | module.exports = router; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const app = require('./server/server'); 2 | const config = require('./server/config/config'); 3 | 4 | // Start listening 5 | app.listen(config.port, () => { 6 | console.log('Sever started http://localhost:%s', config.port); 7 | }); 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true 5 | }, 6 | "extends": "airbnb", 7 | "rules": { 8 | "no-console": [2, {"allow": ["log", "warn", "error"]}], 9 | // "semi": ["error", "always"], 10 | // "no-extra-semi": 2 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /server/api/user/routes.js: -------------------------------------------------------------------------------- 1 | const Router = require('express').Router(); 2 | const controller = require('./controller'); 3 | const auth = require('../../auth/auth'); 4 | 5 | Router.route('/') 6 | .post(controller.saveUser); 7 | 8 | Router.route('/:id') 9 | .get(controller.getUser); 10 | 11 | module.exports = Router; 12 | -------------------------------------------------------------------------------- /server/auth/routes.js: -------------------------------------------------------------------------------- 1 | const Router = require('express').Router(); 2 | const verifyUser = require('./auth').verifyUser; 3 | const controller = require('./controller'); 4 | // const decodeToken = require('./auth').decodeToken; 5 | 6 | // Before we send back JWT, let's check if 7 | // user's email and password match what we have in the database 8 | Router.post('/signin', verifyUser(), controller.signin); 9 | 10 | module.exports = Router; 11 | -------------------------------------------------------------------------------- /server/config/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | dev: 'development', 3 | test: 'test', 4 | prod: 'production', 5 | port: process.env.PORT || 8000, 6 | expireTime: '7d', 7 | secrets: { 8 | jwt: process.env.JWT || 'restosecrets198', 9 | }, 10 | }; 11 | 12 | // Setting environment variable 13 | process.env.NODE_ENV = process.env.NODE_ENV || config.dev; 14 | config.env = process.env.NODE_ENV; 15 | 16 | module.exports = config; 17 | -------------------------------------------------------------------------------- /server/config/middlewares.js: -------------------------------------------------------------------------------- 1 | // Setup middlewares 2 | const morgan = require('morgan'); 3 | const bodyParser = require('body-parser'); 4 | const compression = require('compression'); 5 | const helmet = require('helmet'); 6 | const cors = require('cors'); 7 | 8 | module.exports = (app) => { 9 | app.use(morgan('dev')); 10 | app.use(compression()); 11 | app.use(bodyParser.json()); 12 | app.use(bodyParser.urlencoded({ extended: true })); 13 | app.use(helmet()); 14 | app.use(cors()); 15 | }; 16 | -------------------------------------------------------------------------------- /server/auth/controller.js: -------------------------------------------------------------------------------- 1 | const signToken = require('./auth').signToken; 2 | 3 | module.exports = { 4 | signin, 5 | }; 6 | 7 | function signin(req, res) { 8 | // req.user will be there from the middleware 9 | // verify user. Then we can just create a token 10 | // and send it back for the client to consume 11 | const token = signToken(req.user.id); 12 | return res.json({ 13 | token, 14 | user: { 15 | username: req.user.username, 16 | email: req.user.email, 17 | }, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /seeders/users.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | username: 'alices', 4 | firstName: 'Alice', 5 | lastName: 'Smith', 6 | email: 'alice@cc.cc', 7 | password: 'Pass123!', 8 | }, 9 | { 10 | username: 'bobs', 11 | firstName: 'Bob', 12 | lastName: 'Smith', 13 | email: 'bob@cc.cc', 14 | password: 'Pass123!', 15 | }, 16 | { 17 | username: 'chriss', 18 | firstName: 'Chris', 19 | lastName: 'Smith', 20 | email: 'chris@cc.cc', 21 | password: 'Pass123!', 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "hagio", 4 | "password": "password", 5 | "database": "npas_dev", 6 | "host": "127.0.0.1", 7 | "port": "5432", 8 | "dialect": "postgres" 9 | }, 10 | "test": { 11 | "username": "hagio", 12 | "password": "password", 13 | "database": "npas_test", 14 | "host": "127.0.0.1", 15 | "port": "5432", 16 | "dialect": "postgres" 17 | }, 18 | "production": { 19 | "username": "", 20 | "password": "", 21 | "database": "", 22 | "host": "", 23 | "port": "", 24 | "dialect": "" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | const api = require('./api/api'); 5 | const auth = require('./auth/routes'); 6 | 7 | // Middlewares setup 8 | require('./config/middlewares')(app); 9 | 10 | // Routes 11 | app.use('/api', api); 12 | app.use('/auth', auth); 13 | 14 | app.use((err, req, res) => { 15 | // if error thrown from jwt validation check 16 | if (err.name === 'UnauthorizedError') { 17 | return res.status(401).send({ error: 'Invalid token' }); 18 | } 19 | return res.status(500).send({ error: 'Something went wrong.' }); 20 | }); 21 | 22 | module.exports = app; 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Yuichi Hagio 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var Sequelize = require('sequelize'); 6 | var basename = path.basename(module.filename); 7 | var env = process.env.NODE_ENV || 'development'; 8 | var config = require(__dirname + '/../config/config.json')[env]; 9 | var db = {}; 10 | 11 | if (config.use_env_variable) { 12 | var sequelize = new Sequelize(process.env[config.use_env_variable]); 13 | } else { 14 | var sequelize = new Sequelize(config.database, config.username, config.password, config); 15 | } 16 | 17 | fs 18 | .readdirSync(__dirname) 19 | .filter(function(file) { 20 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 21 | }) 22 | .forEach(function(file) { 23 | var model = sequelize['import'](path.join(__dirname, file)); 24 | db[model.name] = model; 25 | }); 26 | 27 | Object.keys(db).forEach(function(modelName) { 28 | if (db[modelName].associate) { 29 | db[modelName].associate(db); 30 | } 31 | }); 32 | 33 | db.sequelize = sequelize; 34 | db.Sequelize = Sequelize; 35 | 36 | module.exports = db; 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Node.js / Express / PostgreSQL (Sequelize) starter kit 2 | 3 | I created this simple starter kit 4 | to get started working on simple Node.js API project quickly 5 | 6 | * [Node.js](https://nodejs.org/en/) 7 | * [PostgreSQL](https://www.postgresql.org/) 8 | * [Sequelize](http://docs.sequelizejs.com/en/v3/) 9 | * [Sequelize-CLI](https://github.com/sequelize/cli) 10 | * [JSON Web Token](https://jwt.io/) 11 | 12 | **Hosting** 13 | 14 | * [Heroku](https://www.heroku.com/) 15 | 16 | **Testing** 17 | 18 | * [Mocha](https://mochajs.org/) 19 | * [Chai](http://chaijs.com/) 20 | * [Supertest](https://github.com/visionmedia/supertest) 21 | 22 | 23 | ### Features 24 | 25 | * [X] Linting (Airbnb) 26 | * [X] Authentication with JSON Web Token 27 | * [X] Username, Email, Password validations 28 | * [X] User signup, signin 29 | * [X] API and Unit testing 30 | * [X] Easily deployable to Heroku (Procfile) 31 | 32 | ### To run locally 33 | 34 | Make sure to install and run PostgreSQL first. 35 | ``` 36 | brew update 37 | brew install postgres 38 | createdb npas-dev 39 | ``` 40 | 41 | Clone the repo and run the app 42 | ``` 43 | git clone git@github.com:yhagio/node-postgres-api-starter.git nd 44 | cd nd 45 | npm i 46 | npm run start 47 | ``` 48 | 49 | ### To run test 50 | 51 | ``` 52 | npm run test 53 | ``` 54 | 55 | and in another tab 56 | ``` 57 | npm run test:only 58 | ``` 59 | 60 | ### To deploy on Heroku 61 | ``` 62 | heroku login 63 | heroku create 64 | git push heroku master 65 | ``` 66 | 67 | 68 | #### Side Note 69 | 70 | With `sequelize-cli` 71 | ``` 72 | ./node_modules/.bin/sequelize init 73 | ``` 74 | will create model setup and edit `./config/config.json` for your database. 75 | -------------------------------------------------------------------------------- /server/utils/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | validateEmail, 3 | validatePassword, 4 | validateStringLength, 5 | } 6 | 7 | function validateEmail(email) { 8 | let errorMessage = ''; 9 | const regex = /\S+@\S+\.\S+/; 10 | const trimmedEmail = email.trim(); 11 | 12 | if (trimmedEmail.length > 40) { 13 | errorMessage = '* Email is too long, please use shorter email address'; 14 | } 15 | 16 | if (!regex.test(trimmedEmail) || trimmedEmail.length === 0) { 17 | errorMessage = '* Email must be in valid format'; 18 | } 19 | 20 | return errorMessage; 21 | }; 22 | 23 | function validatePassword(password) { 24 | const errorMessages = []; 25 | 26 | if (password.length > 50) { 27 | errorMessages.push('* Must be fewer than 50 chars'); 28 | } 29 | 30 | if (password.length < 8) { 31 | errorMessages.push('* Must be longer than 7 chars'); 32 | } 33 | 34 | if (!password.match(/[\!\@\#\$\%\^\&\*]/g)) { 35 | errorMessages.push('* Missing a symbol(! @ # $ % ^ & *)'); 36 | } 37 | 38 | if (!password.match(/\d/g)) { 39 | errorMessages.push('* Missing a number'); 40 | } 41 | 42 | if (!password.match(/[a-z]/g)) { 43 | errorMessages.push('* Missing a lowercase letter'); 44 | } 45 | 46 | if (!password.match(/[A-Z]/g)) { 47 | errorMessages.push('* Missing an uppercase letter'); 48 | } 49 | 50 | return errorMessages; 51 | }; 52 | 53 | function validateStringLength(text, limit) { 54 | let errorMessage = ''; 55 | if (text.trim().length > limit) { 56 | errorMessage = `* Cannot be more than ${limit} characters`; 57 | } else if (text.trim().length <= 0) { 58 | errorMessage = '* Cannot be empty'; 59 | } else { 60 | errorMessage = ''; 61 | } 62 | return errorMessage; 63 | }; 64 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const User = sequelize.define('User', { 3 | id: { 4 | type: DataTypes.UUID, 5 | defaultValue: DataTypes.UUIDV1, 6 | primaryKey: true, 7 | allowNull: false, 8 | unique: true, 9 | }, 10 | email: { 11 | type: DataTypes.STRING, 12 | allowNull: false, 13 | }, 14 | password: { 15 | type: DataTypes.STRING, 16 | allowNull: false, 17 | }, 18 | username: { 19 | type: DataTypes.STRING, 20 | allowNull: false, 21 | }, 22 | firstName: { 23 | type: DataTypes.STRING, 24 | allowNull: true, 25 | }, 26 | lastName: { 27 | type: DataTypes.STRING, 28 | allowNull: true, 29 | }, 30 | }); 31 | 32 | 33 | // Insert seed users 34 | if (process.env.NODE_ENV === 'development' || 35 | process.env.NODE_ENV === 'test') { 36 | 37 | const bcrypt = require('bcrypt'); 38 | const users = require('../seeders/users'); 39 | 40 | sequelize 41 | .sync() 42 | .then(() => { 43 | User 44 | .findAndCountAll() 45 | .then((result) => { 46 | if (result.count === 0) { 47 | for (let i = 0; i < users.length; i++) { 48 | const salt = bcrypt.genSaltSync(10); 49 | const hash = bcrypt.hashSync(users[i].password, salt); 50 | 51 | User.create({ 52 | firstName: users[i].firstName, 53 | lastName: users[i].lastName, 54 | email: users[i].email, 55 | password: hash, 56 | username: users[i].username, 57 | }); 58 | } 59 | } 60 | }); 61 | }) 62 | .catch((e) => { 63 | console.log('ERROR SYNCING WITH DB: ', e); 64 | }); 65 | } 66 | return User; 67 | }; 68 | -------------------------------------------------------------------------------- /test/auth/auth.test.js: -------------------------------------------------------------------------------- 1 | const app = require('../../server/server'); 2 | const request = require('supertest'); 3 | const expect = require('chai').expect; 4 | 5 | describe('[Authentication] /auth Testing', () => { 6 | it('should be able to sign in with correct credentials', (done) => { 7 | request(app) 8 | .post('/api/users') 9 | .send({ 10 | username: 'alizona', 11 | email: 'alizona@cc.cc', 12 | password: 'Pass123!' 13 | }) 14 | .set('Accept', 'application/json') 15 | .expect('Content-Type', /json/) 16 | .expect(201) 17 | .end((err1, res) => { 18 | // console.log('==========\n', res.body) 19 | request(app) 20 | .post('/auth/signin') 21 | .send({ 22 | email: 'alizona@cc.cc', 23 | password: 'Pass123!' 24 | }) 25 | .set('Accept', 'application/json') 26 | .expect('Content-Type', /json/) 27 | .expect(201) 28 | .end((err2, resp) => { 29 | // console.log('==========\n', resp.body) 30 | expect(resp.body).to.be.an('object'); 31 | expect(resp.body).to.have.deep.property('user.username', 'alizona'); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | 37 | it('should not be able to sign in if credentials are incorrect', (done) => { 38 | request(app) 39 | .post('/auth/signin') 40 | .send({ 41 | email: 'alice@cc.cc', 42 | password:'BadPass123!' 43 | }) 44 | .set('Accept', 'application/json') 45 | .expect('Content-Type', /json/) 46 | .expect(401) 47 | .end((err, res) => { 48 | expect(res.body).to.be.an('object'); 49 | expect(res.body).to.have.property('error'); 50 | expect(res.body).to.have.deep.property('error', 'Incorrect credentials'); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-postgres-api-starter", 3 | "version": "0.1.0", 4 | "description": "Node.js / Express / PostgreSQL / Sequelize / Authentication API Starter kit", 5 | "engines": { 6 | "node": "6.6.0", 7 | "npm": "3.10.8" 8 | }, 9 | "main": "index.js", 10 | "scripts": { 11 | "lint": "eslint server ; exit 0", 12 | "prestart": "psql -c 'DROP DATABASE IF EXISTS npas_dev' && psql -c 'CREATE DATABASE npas_dev'", 13 | "start": "export NODE_ENV=development && node index.js", 14 | "pretest": "psql -c 'DROP DATABASE IF EXISTS npas_test' && psql -c 'CREATE DATABASE npas_test'", 15 | "test": "export NODE_ENV=test && node index.js", 16 | "test:only": "export NODE_ENV=test && ./node_modules/.bin/mocha -R spec 'test/**/**.test.js'", 17 | "coverage": "istanbul cover _mocha -- -R spec 'test/**/**/**/**test.js'" 18 | }, 19 | "keywords": [ 20 | "Node.js", 21 | "Express", 22 | "PostgreSQL", 23 | "Sequelize", 24 | "JWT", 25 | "OAuth", 26 | "REST", 27 | "API" 28 | ], 29 | "author": "Yuichi Hagio (http://github.com/yhagio)", 30 | "license": "MIT", 31 | "dependencies": { 32 | "bcrypt": "^0.8.7", 33 | "body-parser": "^1.15.2", 34 | "compression": "^1.6.2", 35 | "cors": "^2.8.1", 36 | "express": "^4.14.0", 37 | "express-jwt": "^5.1.0", 38 | "helmet": "^3.1.0", 39 | "jsonwebtoken": "^7.1.9", 40 | "pg": "^6.1.0", 41 | "pg-hstore": "^2.3.2", 42 | "sequelize": "^3.27.0", 43 | "sequelize-cli": "^2.4.0" 44 | }, 45 | "devDependencies": { 46 | "chai": "^3.5.0", 47 | "eslint": "^3.11.1", 48 | "eslint-config-airbnb": "^13.0.0", 49 | "eslint-plugin-import": "^2.2.0", 50 | "eslint-plugin-jsx-a11y": "^2.2.3", 51 | "eslint-plugin-react": "^6.7.1", 52 | "istanbul": "^0.4.5", 53 | "mocha": "^3.2.0", 54 | "morgan": "^1.7.0", 55 | "supertest": "^2.0.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/user/user.test.js: -------------------------------------------------------------------------------- 1 | const app = require('../../server/server'); 2 | const request = require('supertest'); 3 | const expect = require('chai').expect; 4 | 5 | describe('[USER] /api/users Testing', () => { 6 | 7 | it('should be able to sign up a new user', (done) => { 8 | request(app) 9 | .post('/api/users') 10 | .send({ 11 | username: 'mufasa', 12 | email: 'mazamba@cc.cc', 13 | password:'Pass123!' 14 | }) 15 | .set('Accept', 'application/json') 16 | .expect('Content-Type', /json/) 17 | .expect(201) 18 | .end((err, res) => { 19 | // console.log('====res.body========\n', res.body) 20 | expect(res.body).to.be.an('object'); 21 | expect(res.body).to.have.property('token'); 22 | expect(res.body).to.have.deep.property('user.email', 'mazamba@cc.cc'); 23 | done(); 24 | }) 25 | }); 26 | 27 | it('should not be able to sign up if any inputs are empty', (done) => { 28 | request(app) 29 | .post('/api/users') 30 | .send({ 31 | username: '', 32 | email: 'olala@cc.cc', 33 | password:'Pass123!' 34 | }) 35 | .set('Accept', 'application/json') 36 | .expect('Content-Type', /json/) 37 | .expect(422) 38 | .end((err, res) => { 39 | expect(res.body).to.be.an('object'); 40 | expect(res.body).to.have.property('error'); 41 | expect(res.body).to.have.deep.property('error', 'Username, email, and password are required.'); 42 | done(); 43 | }) 44 | }); 45 | 46 | it('should not be able to sign up a user with same email', (done) => { 47 | request(app) 48 | .post('/api/users') 49 | .send({ 50 | username: 'kevin', 51 | email: 'alice@cc.cc', 52 | password: 'Pass123!' 53 | }) 54 | .set('Accept', 'application/json') 55 | .expect('Content-Type', /json/) 56 | .expect(422) 57 | .end((err, res) => { 58 | expect(res.body).to.be.an('object'); 59 | expect(res.body).to.have.property('error'); 60 | expect(res.body).to.have.deep.property('error', 'The email is already registered.'); 61 | done(); 62 | }) 63 | }); 64 | }) -------------------------------------------------------------------------------- /test/utils/helpers.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const helpers = require('../../server/utils/helpers'); 4 | 5 | describe('[Helpers] Unit Test - validateStringLength', () => { 6 | it('returns give error if input length > limit', () => { 7 | const error = helpers.validateStringLength('Hello World', 5); 8 | expect(error).to.equal('* Cannot be more than 5 characters'); 9 | }); 10 | 11 | it('returns error if input is empty', () => { 12 | const error = helpers.validateStringLength('', 5); 13 | expect(error).to.equal('* Cannot be empty'); 14 | }); 15 | 16 | it('returns empty string if there is no error', () => { 17 | const error = helpers.validateStringLength('Hello World!', 20); 18 | expect(error).to.equal(''); 19 | }); 20 | }); 21 | 22 | describe('[Helpers] Unit Test - validatePassword', () => { 23 | it('returns error if length > 50 chars', () => { 24 | const password = 'Aas9df8sd9!@#sjsdlkfjdskfdsjfkldslkfjldkfjlkdsjflk dsjfkljdsklfjdsklfjlkdsfdsfddsdfsi4y34y0fhelkjvfsjf934otkshdlf9s8f0sdfsdfsdfuds0u230r9uwefdfssfdsfdsfsdf'; 25 | const error = helpers.validatePassword(password); 26 | expect(error[0]).to.equal('* Must be fewer than 50 chars'); 27 | }); 28 | 29 | it('returns error if length < 8 chars', () => { 30 | const error = helpers.validatePassword('fd4%!D'); 31 | expect(error[0]).to.equal('* Must be longer than 7 chars'); 32 | }); 33 | 34 | it('returns error if missing a symbol', () => { 35 | const error = helpers.validatePassword('fd4fdfdffD'); 36 | expect(error[0]).to.equal('* Missing a symbol(! @ # $ % ^ & *)'); 37 | }); 38 | 39 | it('returns error if missing a number', () => { 40 | const error = helpers.validatePassword('fd!!dfdffD'); 41 | expect(error[0]).to.equal('* Missing a number'); 42 | }); 43 | 44 | it('returns error if missing a lowercase', () => { 45 | const error = helpers.validatePassword('D^&!DSDSDD123'); 46 | expect(error[0]).to.equal('* Missing a lowercase letter'); 47 | }); 48 | 49 | it('returns error if missing a uppercase', () => { 50 | const error = helpers.validatePassword('!(*&*^7dfsdfsf)'); 51 | expect(error[0]).to.equal('* Missing an uppercase letter'); 52 | }); 53 | 54 | it('returns no error if it is in correct format', () => { 55 | const error = helpers.validatePassword('Correct123!'); 56 | expect(error).to.have.lengthOf(0); 57 | }); 58 | }); 59 | 60 | describe('[Helpers] Unit Test - validateEmail', () => { 61 | it('returns error if it is longer than 40 chars', () => { 62 | const error = helpers.validateEmail('Asdfdfdsf4234234324324dsfjsdkflsdkfsdfskdfdsklfsd23434324234fdsfdsfsdf@cc.cc'); 63 | expect(error).to.equal('* Email is too long, please use shorter email address'); 64 | }); 65 | 66 | it('returns no error if it is in correct foramt', () => { 67 | const error = helpers.validateEmail('fsdf.c.c'); 68 | expect(error).to.equal('* Email must be in valid format'); 69 | }); 70 | }); -------------------------------------------------------------------------------- /server/api/user/controller.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | 3 | const User = require('../../../models').User; 4 | const signToken = require('../../auth/auth').signToken; 5 | const validatePassword = require('../../utils/helpers').validatePassword; 6 | const validateEmail = require('../../utils/helpers').validateEmail; 7 | const generatePassword = require('../../utils/helpers').generatePassword; 8 | 9 | module.exports = { 10 | saveUser, 11 | getUser, 12 | }; 13 | 14 | // Register new user 15 | function saveUser(req, res) { 16 | const username = req.body.username ? req.body.username.trim() : ''; 17 | const email = req.body.email ? req.body.email.trim() : ''; 18 | const password = req.body.password ? req.body.password.trim() : ''; 19 | 20 | if (!username || !email || !password) { 21 | return res 22 | .status(422) 23 | .send({ error: 'Username, email, and password are required.' }); 24 | } 25 | 26 | if (username.length > 30) { 27 | return res 28 | .status(400) 29 | .send({ error: 'Username must be less than 30 characters.' }); 30 | } 31 | 32 | const emailValidationError = validateEmail(email); 33 | if (emailValidationError.length > 0) { 34 | return res 35 | .status(400) 36 | .send({ error: emailValidationError }); // array of errors 37 | } 38 | 39 | const passwordValidationError = validatePassword(password); 40 | if (passwordValidationError.length > 0) { 41 | return res 42 | .status(400) 43 | .send({ error: passwordValidationError }); 44 | } 45 | 46 | // Check if email already exists 47 | User.findAll({ 48 | where: { email }, 49 | }) 50 | .then((user) => { 51 | if (user.length > 0) { 52 | return res 53 | .status(400) 54 | .send({ error: 'The email is already registered.' }); 55 | } 56 | 57 | const salt = bcrypt.genSaltSync(10); 58 | const hash = bcrypt.hashSync(password, salt); 59 | 60 | const newUser = { 61 | username, 62 | email, 63 | password: hash, 64 | }; 65 | 66 | User.create(newUser) 67 | .then((data) => { 68 | return res.json({ 69 | token: signToken(data.id), 70 | user: { 71 | id: data.id, 72 | username: data.username, 73 | email: data.email, 74 | }, 75 | }); 76 | }) 77 | .catch(err => res.status(400).send({ error: err.message })); 78 | }) 79 | .catch(err => res.status(400).send({ error: err.message })); 80 | } 81 | 82 | // Get one user 83 | function getUser(req, res) { 84 | User.findById(req.params.id) 85 | .then((user) => { 86 | if (!user || user.email.length <= 0) { 87 | return res.status(400).send({ error: 'No user found' }); 88 | } 89 | return res.json({ 90 | id: user.id, 91 | username: user.username, 92 | firstName: user.firstName, 93 | lastName: user.lastName, 94 | email: user.email, 95 | }); 96 | }) 97 | .catch(err => res.status(400).send({ error: err.message })); 98 | } 99 | -------------------------------------------------------------------------------- /server/auth/auth.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const jwt = require('jsonwebtoken'); 3 | const expressJwt = require('express-jwt'); 4 | 5 | const config = require('../config/config'); 6 | const checkToken = expressJwt({ secret: config.secrets.jwt }); 7 | const User = require('../../models').User; 8 | 9 | module.exports = { 10 | decodeToken, 11 | getFreshUser, 12 | verifyUser, 13 | signToken, 14 | }; 15 | 16 | // Decode user's token 17 | function decodeToken() { 18 | return (req, res, next) => { 19 | // [OPTIONAL] 20 | // make it optional to place token on query string 21 | // if it is, place it on the headers where it should be 22 | // so checkToken can see it. See follow the 'Bearer 034930493' format 23 | // so checkToken can see it and decode it 24 | if (req.query && req.query.hasOwnProperty('access_token')) { 25 | req.headers.authorization = `Bearer ${req.query.access_token}`; 26 | } 27 | // this will call next if token is valid 28 | // and send error if it is not. It will attached 29 | // the decoded token to req.user 30 | return checkToken(req, res, next); 31 | } 32 | }; 33 | 34 | // Set req.user to the authenticated user if JWT is valid & user is found in DB, 35 | // otherwise return error 36 | function getFreshUser() { 37 | return (req, res, next) => { 38 | User.findById(req.user._id) 39 | .then((user) => { 40 | if (!user) { 41 | // if no user is found it was not 42 | // it was a valid JWT but didn't decode 43 | // to a real user in our DB. Either the user was deleted 44 | // since the client got the JWT, or 45 | // it was a JWT from some other source 46 | return res.status(401).send({ error: 'Unauthorized' }); 47 | } 48 | // update req.user with fresh user from 49 | // stale token data 50 | req.user = user; 51 | return next(); 52 | }) 53 | .catch(err => next(err)); 54 | }; 55 | }; 56 | 57 | // Authenticate the user 58 | function verifyUser() { 59 | return (req, res, next) => { 60 | const email = req.body.email; 61 | const password = req.body.password; 62 | 63 | // if no email or password then send 64 | if (!email || !password) { 65 | return res.status(400).send({ error: 'You need valid email and password' }); 66 | } 67 | 68 | // look user up in the DB so we can check 69 | // if the passwords match for the email 70 | User.findAll({ 71 | where: { 72 | email, 73 | }, 74 | }) 75 | .then((user) => { 76 | if (!user[0]) { 77 | return res.status(401).send({ error: 'No user with the given email' }); 78 | } 79 | // checking the passowords 80 | if (!bcrypt.compareSync(password, user[0].password)) { 81 | return res.status(401).send({ error: 'Incorrect credentials' }); 82 | } 83 | // if everything is good, 84 | // then attach to req.user 85 | // and call next so the controller 86 | // can sign a token from the req.user._id 87 | req.user = user[0]; 88 | return next(); 89 | }) 90 | .catch(err => next(err)); 91 | }; 92 | } 93 | 94 | // Sign token on signup 95 | function signToken(id) { 96 | return jwt.sign( 97 | { id }, 98 | config.secrets.jwt, 99 | { expiresIn: config.expireTime } 100 | ); 101 | } 102 | --------------------------------------------------------------------------------