├── .editorconfig ├── .eslintrc.json ├── .example.env ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEAUTRE_REQUEST.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── README.md ├── package-lock.json ├── package.json └── src ├── app.js ├── constants └── responseMessages.js ├── db ├── index.js ├── mongodb.js ├── mysql.js └── redis.js ├── index.js ├── middlewares └── isAuthenticated.js ├── modules └── users │ ├── users.controller.js │ ├── users.request.validators.js │ ├── users.routes.js │ └── users.services.js ├── routes ├── index.js └── v1 │ └── index.js └── utils ├── encryption.js ├── handleCustomErrors.js ├── hashPayload.js ├── index.js ├── logger.js └── sendResponse.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /.example.env: -------------------------------------------------------------------------------- 1 | PORT= 2 | DB_NAME=your_db_name 3 | DB_USERNAME=your_db_username 4 | DB_PASSWORD=your_db_password 5 | DB_HOST= 6 | DB_PORT=3306 7 | DB_DIALECT=mysql 8 | DB_LOGGING=true 9 | DB_POOL_MAX= 10 | DB_POOL_MIN= 11 | DB_CONNECTION_IDLE= 12 | PASS_HASH_ROUNDS= 13 | TOKEN_SECRET=randomsecretforapi 14 | ACCESS_TOKEN_EXPIRY= 15 | ACCESS_TOKEN_ALGO= 16 | REFRESH_TOKEN_EXPIRY= 17 | REFRESH_TOKEN_ALGO= -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knaxus/node-api-template/e8e6f5765b498e110d549c91bdfd22edc10b4c80/.github/CONTRIBUTING.md -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a Bug 3 | about: Let us know the bug you encountered 4 | title: Bug in 5 | labels: 'bug' 6 | assignees: 'ashokdey' 7 | --- 8 | 9 | ## Overview 10 | 11 | Tell us about the bug and attach relavant links (if any) 12 | 13 | ## Explain the Bug 14 | 15 | Tell us more about the bug. 16 | 17 | - How it happened 18 | - What is the environment 19 | - Attach screenshots, stack trace 20 | 21 | ## Suggestions 22 | 23 | Do you have any suggestion or your approach to solve this bug? Let us know 24 | 25 | ## Links 26 | 27 | We want to know you better, feel free to drop your email, website and Linkedin profile 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEAUTRE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Tell us what you want 4 | title: Need For 5 | labels: 'feature' 6 | assignees: 'ashokdey' 7 | --- 8 | 9 | ## Overview 10 | 11 | Tell us about the feature and attach relavant links (if any) 12 | 13 | ## Feature Explanation 14 | 15 | - Why you need this feature? 16 | - Where you need it? 17 | - And can it be a general feature for all use cases? 18 | 19 | ## Benefits 20 | 21 | Mention the benifits of the feature and how it will help this api-temaplte to be scalable and robust 22 | 23 | ## Links 24 | 25 | We want to know you better, feel free to drop your email, website and Linkedin profile 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull Request 3 | about: Let us know the changes you made 4 | title: 5 | labels: '' 6 | assignees: 'ashokdey' 7 | --- 8 | 9 | ## Overview 10 | 11 | Tell us about the changes/improvements/fixes and attach relavant links (if any) 12 | 13 | ## Explain the changes/fixes 14 | 15 | Tell us more about the pull request. 16 | 17 | - What all you changed? 18 | - Did it fix any present isses? [Add issue number(s)] 19 | - Did you remove any package? 20 | 21 | ## Links 22 | 23 | We want to know you better, feel free to drop your email, website and Linkedin profile 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | logs/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scalable API Template for Node.js Applications 2 | 3 | ## How to RUN 4 | 5 | - There are NPM scripts to help your development enviroment easy 6 | 7 | ## NPM Scripts 8 | 9 | - Use `npm run dev` to start the app in development mode 10 | - Go to `http://localhost:PORT_NUMBER` to see the app running 11 | 12 | ## Code Structure 13 | 14 | This codebase follows MVC pattern with few additional layers. 15 | 16 | ### List of layers 17 | 18 | The codebase has following flow of different layers: 19 | 20 | - Routes 21 | - Controllers 22 | - Request Validators 23 | - Service Layer 24 | - Dispatch Response 25 | 26 | ### Routes 27 | 28 | - Routes are the top level and are created using `Express.js` routes 29 | - They internally calls the `controllers` 30 | - Every route has it's own `controller` 31 | 32 | ### Controllers 33 | 34 | - Controller is invoked by it's `route` 35 | - Controllers at it's core has following responsiblities: 36 | - Validation of the client request 37 | - Extract the request entities (body, params or query) 38 | - Invokes different services 39 | - Returns the final response back to the client 40 | 41 | ### Request Validators 42 | 43 | - This layer is a set of custom functions in a separate file 44 | - They are used to validate the client request be it the body, params or query of the request 45 | - If the client request is not desireable, it returns the errors back to the client 46 | 47 | ### Service Layers 48 | 49 | - This layer primarily interacts with the Database 50 | - It is reponsible only for getting the required data from the Database and return it to the controller 51 | - **Note**: All the business logic and query filteration should take place inside the controller 52 | 53 | ## Libraries Used 54 | 55 | - Express.js 56 | - Express Validator 57 | - Mongoose 58 | 59 | ## HTTP Verbs Used 60 | 61 | - **GET** : Use this to fetch data from the DB to client 62 | - **POST**: Use this when creating new record in DB 63 | - **PATCH**: Use this when you partially update any entity 64 | - **DELETE**: Use this when you are performing delete operation 65 | 66 | ## HTTP Status Code Used 67 | 68 | - 200 - Used when you get data successfully 69 | - 201 - Used when your data created successfully 70 | 71 | - 400 - Used when there is bad request from the client 72 | - 401 - Used when user is not authenticated 73 | - 403 - Used when user is authenticated but do not have permissions to access resource 74 | - 404 - Used when data not found 75 | - 422 - Used when payload key(s) is valid but the data in the key(s) are unprocessable 76 | - 500 - Used when server encounter unexpected condition 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-template", 3 | "version": "1.0.0", 4 | "description": "Scalable folder stucture", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=8.9", 8 | "npm": ">=3.10.0" 9 | }, 10 | "engineStrict": true, 11 | "scripts": { 12 | "lint": "./node_modules/.bin/eslint ./src", 13 | "test": "jest --verbose", 14 | "test:watch": "jest --watchAll", 15 | "start": "node src/index.js", 16 | "dev": "nodemon src/index.js", 17 | "prepush": "npm run lint" 18 | }, 19 | "keywords": [], 20 | "author": "Ashok Dey (http://ashokdey.in)", 21 | "license": "MIT", 22 | "dependencies": { 23 | "bcrypt-nodejs": "0.0.3", 24 | "bcryptjs": "^2.4.3", 25 | "cors": "^2.8.5", 26 | "dotenv": "^6.2.0", 27 | "express": "^4.16.4", 28 | "express-validator": "^5.3.1", 29 | "jsonwebtoken": "^8.4.0", 30 | "mongoose": "^5.4.19", 31 | "morgan": "^1.9.1", 32 | "mysql2": "^1.6.4", 33 | "redis": "^2.8.0", 34 | "sequelize": "^4.42.0", 35 | "uuid": "^3.3.2", 36 | "validator": "^10.11.0", 37 | "winston": "^3.2.1" 38 | }, 39 | "devDependencies": { 40 | "eslint": "^5.12.0", 41 | "eslint-config-airbnb-base": "^13.1.0", 42 | "eslint-plugin-import": "^2.14.0", 43 | "husky": "^1.3.1", 44 | "jest": "^24.3.1", 45 | "nodemon": "^1.18.9" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const requestValidator = require('express-validator'); 3 | const cors = require('cors'); 4 | const morgan = require('morgan'); 5 | const dotenv = require('dotenv'); 6 | 7 | const result = dotenv.config(); 8 | if (result.error) { 9 | throw result.error; 10 | } 11 | 12 | // custom modules 13 | const { logger } = require('./utils'); 14 | const { MySQL } = require('./db'); 15 | const allRoutes = require('./routes'); 16 | 17 | const { PORT } = process.env; 18 | 19 | const app = express(); 20 | 21 | app.use(express.json()); 22 | app.use(express.urlencoded({ extended: false })); 23 | app.use(cors()); 24 | app.disable('x-powered-by'); 25 | app.use( 26 | morgan('dev', { 27 | skip: () => app.get('env') === 'test', 28 | stream: logger.stream, 29 | }), 30 | ); 31 | app.use(requestValidator()); 32 | 33 | app.get('/', (req, res) => { 34 | res.status(200).json({ 35 | msg: 'Welcome to User Management Services', 36 | }); 37 | }); 38 | 39 | MySQL.sequelize 40 | .sync() 41 | .then(() => { 42 | app.listen(PORT, () => logger.info(`App running at http://localhost:${PORT}`)); 43 | }) 44 | .catch(err => logger.log('error', err)); 45 | 46 | app.use(allRoutes); 47 | 48 | module.exports = app; 49 | -------------------------------------------------------------------------------- /src/constants/responseMessages.js: -------------------------------------------------------------------------------- 1 | const ReponseMessages = { 2 | genericError: 'Something Broke! Try Again Later.', 3 | genericSuccess: 'Request Successful', 4 | }; 5 | 6 | module.exports = ReponseMessages; 7 | -------------------------------------------------------------------------------- /src/db/index.js: -------------------------------------------------------------------------------- 1 | const MySQL = require('./mysql'); 2 | const MongoDB = require('./mongodb'); 3 | const Redis = require('./redis'); 4 | 5 | module.exports = { 6 | MySQL, 7 | MongoDB, 8 | Redis, 9 | }; 10 | -------------------------------------------------------------------------------- /src/db/mongodb.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { logger } = require('../utils'); 3 | 4 | mongoose.Promise = global.Promise; 5 | 6 | // const MONGODB_URI = `mongodb://${username}:${password}@${host}:${port}/${databaseName}`; 7 | 8 | const options = { 9 | reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect 10 | reconnectInterval: 500, // Reconnect every 500ms 11 | useNewUrlParser: true, 12 | }; 13 | 14 | if (!process.env.MONGODB_URI) { 15 | logger.error('Please set MONGO_URI'); 16 | process.exit(-1); 17 | } 18 | 19 | mongoose.connect(process.env.MONGODB_URI, options); 20 | 21 | // mongoose.connect(process.env.MONGODB_URI, { 22 | // auth: { 23 | // user: username, 24 | // password: password 25 | // }, 26 | // options, 27 | // }); 28 | 29 | mongoose.connection.on('connected', () => { 30 | logger.info('Connected to MongoDB'); 31 | }); 32 | 33 | mongoose.connection.on('error', (err) => { 34 | logger.error('MongoDB connection error:', err); 35 | process.exit(-1); 36 | }); 37 | 38 | mongoose.connection.on('disconnected', () => { 39 | logger.error('MongoDB disconnected'); 40 | }); 41 | 42 | module.exports = mongoose; 43 | -------------------------------------------------------------------------------- /src/db/mysql.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | const { logger } = require('../utils'); 3 | 4 | if ( 5 | !process.env.DB_NAME 6 | || !process.env.DB_USERNAME 7 | || !process.env.DB_PASSWORD 8 | || !process.env.DB_HOST 9 | || !process.env.DB_PORT 10 | || !process.env.DB_DIALECT 11 | ) { 12 | logger.error('Please set MySQL ENV variables'); 13 | process.exit(-1); 14 | } 15 | 16 | const db = {}; 17 | 18 | const sequelize = new Sequelize( 19 | process.env.DB_NAME, 20 | process.env.DB_USERNAME, 21 | process.env.DB_PASSWORD, 22 | { 23 | host: process.env.DB_HOST, 24 | port: process.env.DB_PORT, 25 | dialect: process.env.DB_DIALECT, 26 | logging: process.env.DB_LOGGING !== true ? logger.log : false, 27 | benchmark: true, 28 | pool: { 29 | max: process.env.DB_POOL_MAX, 30 | min: process.env.DB_POOL_MIN, 31 | idle: process.env.DB_CONNECTION_IDLE, 32 | }, 33 | operatorsAliases: false, // to supress the deprecation warning 34 | }, 35 | ); 36 | 37 | db.sequelize = sequelize; 38 | db.Sequelize = Sequelize; 39 | 40 | module.exports = db; 41 | -------------------------------------------------------------------------------- /src/db/redis.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis'); 2 | const { logger } = require('../utils'); 3 | 4 | // connect to Redis 5 | const { REDIS_URL } = process.env; 6 | const client = redis.createClient(REDIS_URL); 7 | 8 | client.on('connect', () => { 9 | logger.info('Connected to REDIS'); 10 | }); 11 | 12 | client.on('error', (err) => { 13 | logger.error(`Error connecting REDIS: ${err}`); 14 | }); 15 | 16 | module.exports = client; 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require('./app'); 2 | -------------------------------------------------------------------------------- /src/middlewares/isAuthenticated.js: -------------------------------------------------------------------------------- 1 | const { sendResponse, jwt } = require('../utils'); 2 | 3 | async function isAuthenticated(req, res, next) { 4 | const token = req.header('x-auth-token'); 5 | 6 | try { 7 | if (!token) { 8 | return sendResponse(res, 401, { tokenExpired: 0 }, 'Failed to Authenticate'); 9 | } 10 | 11 | const decoded = jwt.decryptAccessToken(token); 12 | 13 | // if everything is good, save to request for use in other routes 14 | req.user = decoded; 15 | next(); 16 | } catch (err) { 17 | if (err.name === 'TokenExpiredError') { 18 | return sendResponse(res, 401, { tokenExpired: 1 }, 'Token Expired'); 19 | } 20 | if (err.name === 'JsonWebTokenError') { 21 | return sendResponse(res, 401, { tokenExpired: 0 }, 'Corrupt Token'); 22 | } 23 | } 24 | return 0; 25 | } 26 | 27 | module.exports = isAuthenticated; 28 | -------------------------------------------------------------------------------- /src/modules/users/users.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | validateLoginRequest, 3 | validateCreateUserRequest, 4 | validateChangeEmailRequest, 5 | validateChangePasswordRequest, 6 | } = require('./users.request.validators'); 7 | const { 8 | loginUser, 9 | createNewUser, 10 | changeUserEmail, 11 | changeUserPassword, 12 | } = require('./users.services'); 13 | const { sendResponse, handleCustomError } = require('../../utils'); 14 | const ResponseMessages = require('../../constants/responseMessages'); 15 | 16 | async function createNewUserController(req, res) { 17 | try { 18 | const validationErr = validateCreateUserRequest(req); 19 | if (validationErr) { 20 | return sendResponse(res, 422, {}, validationErr[0].msg); 21 | } 22 | 23 | const { 24 | email, firstName, lastName, password, 25 | } = req.body; 26 | 27 | const data = await createNewUser({ 28 | email, 29 | firstName, 30 | lastName, 31 | password, 32 | }); 33 | return sendResponse(res, 201, { ...data }, ResponseMessages.genericSuccess); 34 | } catch (err) { 35 | return handleCustomError(res, err); 36 | } 37 | } 38 | 39 | async function loginUserController(req, res) { 40 | try { 41 | const validationErr = validateLoginRequest(req); 42 | if (validationErr) { 43 | return sendResponse(res, 422, {}, validationErr[0].msg); 44 | } 45 | 46 | const { email, password } = req.body; 47 | 48 | const data = await loginUser({ 49 | email, 50 | password, 51 | }); 52 | return sendResponse(res, 200, { ...data }, ResponseMessages.genericSuccess); 53 | } catch (err) { 54 | return handleCustomError(res, err); 55 | } 56 | } 57 | 58 | async function changeUserEmailController(req, res) { 59 | try { 60 | const validationErr = validateChangeEmailRequest(req); 61 | if (validationErr) { 62 | return sendResponse(res, 422, {}, validationErr[0].msg); 63 | } 64 | 65 | const { oldEmail, newEmail, password } = req.body; 66 | const { id: userId } = req.user; 67 | 68 | const data = await changeUserEmail({ 69 | userId, 70 | oldEmail, 71 | newEmail, 72 | password, 73 | }); 74 | return sendResponse(res, 200, { ...data }, ResponseMessages.genericSuccess); 75 | } catch (err) { 76 | return handleCustomError(res, err); 77 | } 78 | } 79 | 80 | async function changeUserPasswordController(req, res) { 81 | try { 82 | const validationErr = validateChangePasswordRequest(req); 83 | if (validationErr) { 84 | return sendResponse(res, 422, {}, validationErr[0].msg); 85 | } 86 | 87 | const { oldPassword, newPassword } = req.body; 88 | const { id: userId } = req.user; 89 | 90 | const data = await changeUserPassword({ 91 | userId, 92 | oldPassword, 93 | newPassword, 94 | }); 95 | return sendResponse(res, 200, { ...data }, ResponseMessages.genericSuccess); 96 | } catch (err) { 97 | return handleCustomError(res, err); 98 | } 99 | } 100 | 101 | module.exports = { 102 | createNewUserController, 103 | loginUserController, 104 | changeUserEmailController, 105 | changeUserPasswordController, 106 | }; 107 | -------------------------------------------------------------------------------- /src/modules/users/users.request.validators.js: -------------------------------------------------------------------------------- 1 | function validateLoginRequest(req) { 2 | req 3 | .checkBody('email', 'user email is required/invalid') 4 | .isEmail() 5 | .exists(); 6 | req 7 | .checkBody('password', 'user password is required') 8 | .isLength({ min: 6 }) 9 | .exists(); 10 | return req.validationErrors(); 11 | } 12 | 13 | function validateCreateUserRequest(req) { 14 | req 15 | .checkBody('email', 'user email is required/invalid') 16 | .isEmail() 17 | .exists(); 18 | req 19 | .checkBody('password', 'user password is required') 20 | .isLength({ min: 6 }) 21 | .exists(); 22 | req 23 | .checkBody('firstName', 'user firstName is required') 24 | .isString() 25 | .isLength({ min: 3 }) 26 | .exists(); 27 | req 28 | .checkBody('lastName', 'user lastName is required') 29 | .isString() 30 | .isLength({ min: 3 }) 31 | .exists(); 32 | return req.validationErrors(); 33 | } 34 | 35 | function validateChangeEmailRequest(req) { 36 | req 37 | .checkBody('oldEmail', 'user oldEmail is required/invalid') 38 | .isEmail() 39 | .exists(); 40 | req 41 | .checkBody('newEmail', 'user newEmail is required/invalid') 42 | .isEmail() 43 | .exists(); 44 | req 45 | .checkBody('password', 'user password is required') 46 | .isLength({ min: 6 }) 47 | .exists(); 48 | return req.validationErrors(); 49 | } 50 | 51 | function validateChangePasswordRequest(req) { 52 | req 53 | .checkBody('oldPassword', 'user oldPassword is required') 54 | .isLength({ min: 6 }) 55 | .exists(); 56 | req 57 | .checkBody('newPassword', 'user newPassword is required') 58 | .isLength({ min: 6 }) 59 | .exists(); 60 | return req.validationErrors(); 61 | } 62 | 63 | module.exports = { 64 | validateLoginRequest, 65 | validateCreateUserRequest, 66 | validateChangeEmailRequest, 67 | validateChangePasswordRequest, 68 | }; 69 | -------------------------------------------------------------------------------- /src/modules/users/users.routes.js: -------------------------------------------------------------------------------- 1 | const userRoutes = require('express').Router(); 2 | const { 3 | loginUserController, 4 | createNewUserController, 5 | changeUserEmailController, 6 | changeUserPasswordController, 7 | } = require('./users.controller'); 8 | const isAuthenticated = require('../../middlewares/isAuthenticated'); 9 | 10 | userRoutes.post('/users/login', loginUserController); 11 | userRoutes.post('/users/new', createNewUserController); 12 | userRoutes.patch('/users/email', isAuthenticated, changeUserEmailController); 13 | userRoutes.patch('/users/password', isAuthenticated, changeUserPasswordController); 14 | 15 | module.exports = userRoutes; 16 | -------------------------------------------------------------------------------- /src/modules/users/users.services.js: -------------------------------------------------------------------------------- 1 | const { MySQL } = require('../../db'); 2 | const { hashPayload, jwt } = require('../../utils'); 3 | 4 | async function createNewUser({ 5 | email, password, firstName, lastName, 6 | }) { 7 | const hashedPassword = await hashPayload(password); 8 | const user = await MySQL.sequelize.query( 9 | 'INSERT INTO users (email, password, first_name, last_name) VALUES (?, ?, ?, ?)', 10 | { 11 | type: MySQL.sequelize.QueryTypes.INSERT, 12 | replacements: [email, hashedPassword, firstName, lastName], 13 | }, 14 | ); 15 | return { 16 | user: { 17 | id: user[0], 18 | email, 19 | firstName, 20 | lastName, 21 | }, 22 | }; 23 | } 24 | 25 | async function loginUser({ email, password }) { 26 | const hashedPassword = await hashPayload(password); 27 | 28 | const res = await MySQL.sequelize.query('SELECT * FROM users WHERE email = ?', { 29 | type: MySQL.sequelize.QueryTypes.SELECT, 30 | replacements: [email], 31 | }); 32 | 33 | // console.log('---res ---', res); 34 | 35 | if (!res[0]) { 36 | const err = new Error('User Not found'); 37 | err.code = 404; 38 | err.msg = 'User not found in records'; 39 | throw err; 40 | } 41 | 42 | if (res[0].password !== hashedPassword) { 43 | const msg = 'Error in Email/Password'; 44 | const err = new Error(msg); 45 | err.code = 404; 46 | err.msg = msg; 47 | throw err; 48 | } 49 | 50 | const accessToken = jwt.createAccessToken({ 51 | id: res[0].id, 52 | email: res[0].email, 53 | mobile: res[0].mobile, 54 | tokenType: 'LoginToken', 55 | }); 56 | 57 | delete res[0].password; 58 | delete res[0].created_at; 59 | delete res[0].updated_at; 60 | 61 | return { 62 | user: res[0], 63 | token: accessToken, 64 | }; 65 | } 66 | 67 | async function changeUserPassword({ userId, oldPassword, newPassword }) { 68 | const res = await MySQL.sequelize.query('SELECT * FROM users WHERE id = ?', { 69 | type: MySQL.sequelize.QueryTypes.SELECT, 70 | replacements: [userId], 71 | }); 72 | 73 | // console.log('---res ---', res); 74 | 75 | if (!res[0]) { 76 | const msg = 'User not found in records'; 77 | const err = new Error(msg); 78 | err.code = 404; 79 | err.msg = msg; 80 | throw err; 81 | } 82 | 83 | if (res[0].is_active || res[0].is_blocked || res[0].is_deleted) { 84 | const msg = 'User is not allowed to perform any action. Account is susspended'; 85 | const err = new Error(msg); 86 | err.code = 403; 87 | err.msg = msg; 88 | throw err; 89 | } 90 | 91 | const oldHashedPassword = await hashPayload(oldPassword); 92 | 93 | if (res[0].password !== oldHashedPassword) { 94 | const msg = 'Incorrect credential, Not allowed'; 95 | const err = new Error(msg); 96 | err.code = 401; 97 | err.msg = msg; 98 | throw err; 99 | } 100 | 101 | const newHashedPassword = await hashPayload(newPassword); 102 | await MySQL.sequelize.query('UPDATE users SET password = ? WHERE id = ?', { 103 | type: MySQL.sequelize.QueryTypes.UPDATE, 104 | replacements: [newHashedPassword, userId], 105 | }); 106 | return {}; 107 | } 108 | 109 | async function changeUserEmail({ 110 | userId, oldEmail, newEmail, password, 111 | }) { 112 | const res = await MySQL.sequelize.query('SELECT * FROM users WHERE id = ?', { 113 | type: MySQL.sequelize.QueryTypes.SELECT, 114 | replacements: [userId], 115 | }); 116 | 117 | // console.log('---res ---', res[0]); 118 | 119 | if (!res[0]) { 120 | const msg = 'User not found in records'; 121 | const err = new Error(msg); 122 | err.code = 404; 123 | err.msg = msg; 124 | throw err; 125 | } 126 | 127 | if (res[0].email !== oldEmail) { 128 | const msg = 'Invalid userId and userEmail combination'; 129 | const err = new Error(msg); 130 | err.code = 401; 131 | err.msg = msg; 132 | throw err; 133 | } 134 | 135 | if (res[0].is_active || res[0].is_blocked || res[0].is_deleted) { 136 | const msg = 'User is not allowed to perform any action. Account is susspended'; 137 | const err = new Error(msg); 138 | err.code = 403; 139 | err.msg = msg; 140 | throw err; 141 | } 142 | 143 | const hashedPassword = await hashPayload(password); 144 | 145 | if (res[0].password !== hashedPassword) { 146 | const msg = 'Incorrect credential, Not allowed'; 147 | const err = new Error(msg); 148 | err.code = 401; 149 | err.msg = msg; 150 | throw err; 151 | } 152 | 153 | await MySQL.sequelize.query('UPDATE users SET email = ? WHERE id = ?', { 154 | type: MySQL.sequelize.QueryTypes.UPDATE, 155 | replacements: [newEmail, userId], 156 | }); 157 | return {}; 158 | } 159 | 160 | module.exports = { 161 | createNewUser, 162 | loginUser, 163 | changeUserPassword, 164 | changeUserEmail, 165 | }; 166 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const allRoutes = require('express').Router(); 2 | const v1Routes = require('./v1'); 3 | 4 | allRoutes.use('/v1', v1Routes); 5 | 6 | module.exports = allRoutes; 7 | -------------------------------------------------------------------------------- /src/routes/v1/index.js: -------------------------------------------------------------------------------- 1 | const allRoutes = require('express').Router(); 2 | const userRoutes = require('../../modules/users/users.routes'); 3 | 4 | allRoutes.use(userRoutes); 5 | 6 | module.exports = allRoutes; 7 | -------------------------------------------------------------------------------- /src/utils/encryption.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const { logger } = require('.'); 3 | 4 | if ( 5 | !process.env.TOKEN_SECRET 6 | || !process.env.ACCESS_TOKEN_EXPIRY 7 | || !process.env.ACCESS_TOKEN_ALGO 8 | || !process.env.REFRESH_TOKEN_EXPIRY 9 | || !process.env.REFRESH_TOKEN_ALGO 10 | ) { 11 | logger.error('Please set JWT ENV variables'); 12 | process.exit(-1); 13 | } 14 | 15 | const createAccessToken = data => jwt.sign(data, process.env.TOKEN_SECRET, { 16 | expiresIn: process.env.ACCESS_TOKEN_EXPIRY, 17 | algorithm: process.env.ACCESS_TOKEN_ALGO, 18 | }); 19 | 20 | const decryptAccessToken = token => jwt.decode(token, process.env.TOKEN_SECRET, { 21 | expiresIn: process.env.ACCESS_TOKEN_EXPIRY, 22 | algorithm: process.env.ACCESS_TOKEN_ALGO, 23 | }); 24 | 25 | const createaRefreshToken = data => jwt.sign(data, process.env.TOKEN_SECRET, { 26 | expiresIn: process.env.REFRESH_TOKEN_EXPIRY, 27 | algorithm: process.env.REFRESH_TOKEN_ALGO, 28 | }); 29 | 30 | const decryptRefreshToken = token => jwt.decode(token, process.env.TOKEN_SECRET, { 31 | expiresIn: process.env.REFRESH_TOKEN_EXPIRY, 32 | algorithm: process.env.REFRESH_TOKEN_ALGO, 33 | }); 34 | 35 | module.exports = { 36 | createAccessToken, 37 | decryptAccessToken, 38 | createaRefreshToken, 39 | decryptRefreshToken, 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/handleCustomErrors.js: -------------------------------------------------------------------------------- 1 | const sendResponse = require('./sendResponse'); 2 | const logger = require('./logger'); 3 | 4 | function handleCustomThrow(res, error) { 5 | logger.log('error', error); 6 | if (error.parent && error.parent.code === 'ER_DUP_ENTRY') { 7 | return sendResponse(res, 409, {}, 'Duplicate entry'); 8 | } 9 | if (error.code === 400) { 10 | return sendResponse(res, error.code, {}, error.msg || error.message); 11 | } 12 | if (error.code === 401) { 13 | return sendResponse(res, error.code, {}, error.msg || error.message); 14 | } 15 | if (error.code === 403) { 16 | return sendResponse(res, error.code, {}, error.msg || error.message); 17 | } 18 | if (error.code === 404) { 19 | return sendResponse(res, error.code, {}, error.msg || error.message); 20 | } 21 | return sendResponse(res, 500, {}, 'Something went wrong'); 22 | } 23 | 24 | module.exports = handleCustomThrow; 25 | -------------------------------------------------------------------------------- /src/utils/hashPayload.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | async function generateHash(payload) { 4 | const hash = await crypto 5 | .createHash('sha512') 6 | .update(payload) 7 | .digest('hex'); 8 | return hash; 9 | } 10 | 11 | module.exports = generateHash; 12 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | const hashPayload = require('./hashPayload'); 2 | const sendResponse = require('./sendResponse'); 3 | const handleCustomError = require('./handleCustomErrors'); 4 | const { 5 | createAccessToken, 6 | decryptAccessToken, 7 | createaRefreshToken, 8 | decryptRefreshToken, 9 | } = require('./encryption'); 10 | const logger = require('./logger'); 11 | 12 | module.exports = { 13 | hashPayload, 14 | sendResponse, 15 | handleCustomError, 16 | jwt: { 17 | createAccessToken, 18 | decryptAccessToken, 19 | createaRefreshToken, 20 | decryptRefreshToken, 21 | }, 22 | logger, 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | const path = require('path'); 3 | 4 | // const alignedWithColorsAndTime = winston.format.combine( 5 | // winston.format.colorize(), 6 | // winston.format.timestamp(), 7 | // winston.format.align(), 8 | // winston.format.printf((info) => { 9 | // const { 10 | // timestamp, level, message, ...args 11 | // } = info; 12 | 13 | // const ts = timestamp.slice(0, 19).replace('T', ' '); 14 | // return `${ts} [${level}]: ${message} ${ 15 | // Object.keys(args).length ? JSON.stringify(args, null, 2) : '' 16 | // }`; 17 | // }), 18 | // ); 19 | 20 | const logger = winston.createLogger({ 21 | level: 'info', 22 | format: winston.format.combine( 23 | winston.format.colorize(), 24 | winston.format.timestamp(), 25 | winston.format.json(), 26 | ), 27 | defaultMeta: { service: 'user-service' }, 28 | colorize: true, 29 | transports: [ 30 | // 31 | // - Write to all logs with level `info` and below to `combined.log` 32 | // - Write all logs error (and below) to `error.log`. 33 | // 34 | new winston.transports.File({ 35 | filename: path.resolve(__dirname, '../../logs/error.log'), 36 | level: 'error', 37 | }), 38 | new winston.transports.File({ filename: path.resolve(__dirname, '../../logs/combined.log') }), 39 | ], 40 | }); 41 | 42 | // 43 | // If we're not in production then log to the `console` with the format: 44 | // `${info.level}: ${info.message} JSON.stringify({ ...rest }) ` 45 | // 46 | if (process.env.NODE_ENV !== 'production') { 47 | logger.add( 48 | new winston.transports.Console({ 49 | format: winston.format.simple(), 50 | }), 51 | ); 52 | } 53 | 54 | // create a stream object with a 'write' function that will be used by `morgan` 55 | logger.stream = { 56 | // write(message, encoding) { 57 | write(message) { 58 | // use the 'info' log level so the output will be 59 | // picked up by both transports (file and console) 60 | logger.info(message); 61 | }, 62 | }; 63 | 64 | module.exports = logger; 65 | -------------------------------------------------------------------------------- /src/utils/sendResponse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function 3 | * 4 | * This function is a response sending wrapper 5 | * Instead of writing extra repetitive lines 6 | * use this wrapper 7 | * 8 | * Made with DRY in mind 9 | * 10 | * @author Ashok Dey 11 | * 12 | * @param {object} res the response object 13 | * @param {number} statusCode the http status code 14 | * @param {array | object } data the data you want to send with the response 15 | * @param {string} message the message you want to send for success/failure 16 | */ 17 | 18 | function sendResponse(res, statusCode, data = {}, message) { 19 | if (typeof statusCode !== 'number') { 20 | throw new Error('statusCode should be a number'); 21 | } 22 | 23 | // status variable to store the status of the response either success or failed 24 | let status = null; 25 | 26 | // regex pattern to validate that the status code is always 3 digits in length 27 | const lengthPattern = /^[0-9]{3}$/; 28 | 29 | // check for the length of the status code, if its 3 then set default value for status as 30 | // failed 31 | // else throw an error 32 | if (!lengthPattern.test(statusCode)) { 33 | throw new Error('Invalid Status Code'); 34 | } 35 | 36 | // regex to test that status code start with 2 or 3 and should me 3 digits in length 37 | const pattern = /^(2|3)\d{2}$/; 38 | 39 | // if the status code starts with 2, set satus variable as success 40 | // eslint-disable-next-line no-unused-expressions 41 | pattern.test(statusCode) ? (status = 'success') : (status = 'failed'); 42 | 43 | return res.status(statusCode).json({ 44 | status, 45 | data, 46 | message, 47 | }); 48 | } 49 | 50 | module.exports = sendResponse; 51 | --------------------------------------------------------------------------------