├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── bin └── www.js ├── config ├── appconfig.js └── config.json ├── controllers ├── AuthController.js ├── BaseController.js └── UsersController.js ├── gulpfile.js ├── migrations ├── 20181022121627-create-users.js ├── 20181104092046-create-roles.js └── 20181104092840-create-permissions.js ├── models ├── index.js ├── permissions.js ├── roles.js └── users.js ├── package-lock.json ├── package.json ├── router ├── api │ ├── authRouter.js │ ├── index.js │ ├── sendEmail.js │ └── usersRouter.js └── index.js ├── server └── index.js └── utils ├── RequestHandler.js ├── auth.js ├── email.js ├── errors.js ├── logger.js ├── stringUtil.js └── swagger.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "airbnb-base" 8 | ], 9 | "rules": { 10 | "indent": [ 11 | 2, 12 | "tab" 13 | ], 14 | "no-tabs": 0, 15 | "no-param-reassign": 0, 16 | "import/no-dynamic-require": 0 17 | } 18 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ala'a Mezian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeJS Project Structure 2 | 3 | Recently I have started working on a new project and the issue that I faced was spending a lot of time building the project structure based on the best practices, especially with JavaScript/NodeJS that has a lot of approaches. I couldn't find any place that wraps the best practices into a single project, so I decided to make it on my own. 4 | 5 | In this repository, I don't aim to provide any optimal solution as each project have its own necessity but to help anyone that is starting with a NodeJS project and can't find any inspiration on how to start building the project to take this project as the starting point. 6 | 7 | Some of the good practices followed in this repository: 8 | - `async` & `await` support 9 | - WinstonJS logger implementation 10 | - Error Handling 11 | - Sequelize Support 12 | - Basic Joi Validation 13 | - Open API specification implemented through swagger-jsdocs and swagger-ui 14 | - JWT implementation 15 | - Enviroment variables to hold configuration values .env file 16 | - OOP (Object-Oriented Programming) 17 | - I followed [airbnb](https://github.com/airbnb/javascript) coding standard with eslint, to help keep things into prespective. 18 | 19 | ### How to start the project: 20 | 21 | - First, you clone the project by using the following command:\ 22 | `git clone https://github.com/AlaaMezian/NodeJs-backend-structure.git` 23 | - Install node version 8.11.0 or use nvm to downgrade your node version. 24 | - Delete the existing `package.lock.json` and run `npm install` 25 | - Then you create a postgres database named iLrn with the following credentials: 26 | ```bash 27 | username: postgres 28 | password: password 29 | ``` 30 | - Run the migration using the following command: 31 | `npx sequelize-cli db:migrate` 32 | - Lastly, you run `npm start` 33 | 34 | Future improvements utilize component based structure.\ 35 | Please feel free to :star: and happy programming :v: 36 | -------------------------------------------------------------------------------- /bin/www.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | 8 | const http = require('http'); 9 | const app = require('../server/index'); 10 | const Logger = require('../utils/logger'); 11 | 12 | const logger = new Logger(); 13 | 14 | /** 15 | * Create HTTP server. 16 | */ 17 | const server = http.createServer(app); 18 | 19 | /** 20 | * Normalize a port into a number, string, or false. 21 | */ 22 | 23 | function normalizePort(val) { 24 | const port = parseInt(val, 10); 25 | 26 | if (Number.isNaN(port)) { 27 | // named pipe 28 | return val; 29 | } 30 | 31 | if (port >= 0) { 32 | // port number 33 | return port; 34 | } 35 | 36 | return false; 37 | } 38 | /** 39 | * Get port from environment and store in Express. 40 | */ 41 | 42 | const port = normalizePort(process.env.DEV_APP_PORT || '3000'); 43 | app.set('port', port); 44 | /** 45 | * Event listener for HTTP server "error" event. 46 | */ 47 | 48 | 49 | function onError(error) { 50 | if (error.syscall !== 'listen') { 51 | throw error; 52 | } 53 | 54 | const bind = typeof port === 'string' 55 | ? `Pipe ${port}` 56 | : `Port ${port}`; 57 | 58 | // handle specific listen errors with friendly messages 59 | switch (error.code) { 60 | case 'EACCES': 61 | logger.log(`${bind} requires elevated privileges`); 62 | process.exit(1); 63 | break; 64 | case 'EADDRINUSE': 65 | logger.log(`${bind} is already in use`); 66 | process.exit(1); 67 | break; 68 | default: 69 | throw error; 70 | } 71 | } 72 | 73 | /** 74 | * Event listener for HTTP server "listening" event. 75 | */ 76 | 77 | function onListening() { 78 | const addr = server.address(); 79 | const bind = typeof addr === 'string' 80 | ? `pipe ${addr}` 81 | : `port ${addr.port}`; 82 | 83 | logger.log(`the server started listining on port ${bind}`, 'info'); 84 | } 85 | 86 | /** 87 | * Listen on provided port, on all network interfaces. 88 | */ 89 | 90 | server.listen(port); 91 | server.on('error', onError); 92 | server.on('listening', onListening); 93 | -------------------------------------------------------------------------------- /config/appconfig.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | require('dotenv').config(); 4 | 5 | // config.js 6 | module.exports = { 7 | app: { 8 | port: process.env.DEV_APP_PORT || 3000, 9 | appName: process.env.APP_NAME || 'iLrn', 10 | env: process.env.NODE_ENV || 'development', 11 | }, 12 | db: { 13 | port: process.env.DB_PORT || 5432, 14 | database: process.env.DB_NAME || 'iLrn', 15 | password: process.env.DB_PASS || 'password', 16 | username: process.env.DB_USER || 'postgres', 17 | host: process.env.DB_HOST || '127.0.0.1', 18 | dialect: 'postgres', 19 | logging: true, 20 | }, 21 | winiston: { 22 | logpath: '/iLrnLogs/logs/', 23 | }, 24 | auth: { 25 | jwt_secret: process.env.JWT_SECRET, 26 | jwt_expiresin: process.env.JWT_EXPIRES_IN || '1d', 27 | saltRounds: process.env.SALT_ROUND || 10, 28 | refresh_token_secret: process.env.REFRESH_TOKEN_SECRET || 'VmVyeVBvd2VyZnVsbFNlY3JldA==', 29 | refresh_token_expiresin: process.env.REFRESH_TOKEN_EXPIRES_IN || '2d', // 2 days 30 | }, 31 | sendgrid: { 32 | api_key: process.env.SEND_GRID_API_KEY, 33 | api_user: process.env.USERNAME, 34 | from_email: process.env.FROM_EMAIL || 'alaa.mezian.mail@gmail.com', 35 | }, 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "url" : "postgres://postgres:password@localhost:5432/iLrn", 4 | "username": "postgres", 5 | "password": "password", 6 | "database": "iLrn", 7 | "host": "127.0.0.1", 8 | "port": 5432, 9 | "dialect": "postgres" 10 | }, 11 | "test": { 12 | "url" : "postgres://postgres:password@localhost:5432/iLrn", 13 | "username": "postgres", 14 | "password": "password", 15 | "database": "iLrn", 16 | "host": "127.0.0.1", 17 | "port": 5432, 18 | "dialect": "postgres" 19 | }, 20 | "production": { 21 | "url" : "postgres://postgres:password@localhost:5432/iLrn", 22 | "username": "postgres", 23 | "password": "password", 24 | "database": "iLrn", 25 | "host": "127.0.0.1", 26 | "port": 5432, 27 | "dialect": "postgres" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /controllers/AuthController.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const bcrypt = require('bcrypt'); 3 | const _ = require('lodash'); 4 | const async = require('async'); 5 | const jwt = require('jsonwebtoken'); 6 | const RequestHandler = require('../utils/RequestHandler'); 7 | const Logger = require('../utils/logger'); 8 | const BaseController = require('../controllers/BaseController'); 9 | const stringUtil = require('../utils/stringUtil'); 10 | const email = require('../utils/email'); 11 | const config = require('../config/appconfig'); 12 | const auth = require('../utils/auth'); 13 | 14 | const logger = new Logger(); 15 | const requestHandler = new RequestHandler(logger); 16 | const tokenList = {}; 17 | 18 | class AuthController extends BaseController { 19 | static async login(req, res) { 20 | try { 21 | const schema = { 22 | email: Joi.string().email().required(), 23 | password: Joi.string().required(), 24 | fcmToken: Joi.string(), 25 | platform: Joi.string().valid('ios', 'android', 'web').required(), 26 | }; 27 | const { error } = Joi.validate({ 28 | email: req.body.email, 29 | password: req.body.password, 30 | fcmToken: req.body.fcmToken, 31 | platform: req.headers.platform, 32 | }, schema); 33 | requestHandler.validateJoi(error, 400, 'bad Request', error ? error.details[0].message : ''); 34 | const options = { 35 | where: { email: req.body.email }, 36 | }; 37 | const user = await super.getByCustomOptions(req, 'Users', options); 38 | if (!user) { 39 | requestHandler.throwError(400, 'bad request', 'invalid email address')(); 40 | } 41 | 42 | if (req.headers.fcmtoken && req.headers.platform) { 43 | const find = { 44 | where: { 45 | user_id: user.id, 46 | fcmToken: req.headers.fcmtoken, 47 | }, 48 | }; 49 | 50 | const fcmToken = await super.getByCustomOptions(req, 'UserTokens', find); 51 | const data = { 52 | userId: user.id, 53 | fcmToken: req.headers.fcmtoken, 54 | platform: req.headers.platform, 55 | }; 56 | 57 | if (fcmToken) { 58 | req.params.id = fcmToken.id; 59 | await super.updateById(req, 'UserTokens', data); 60 | } else { 61 | await super.create(req, 'UserTokens', data); 62 | } 63 | } else { 64 | requestHandler.throwError(400, 'bad request', 'please provide all required headers')(); 65 | } 66 | 67 | await bcrypt 68 | .compare(req.body.password, user.password) 69 | .then( 70 | requestHandler.throwIf(r => !r, 400, 'incorrect', 'failed to login bad credentials'), 71 | requestHandler.throwError(500, 'bcrypt error'), 72 | ); 73 | const data = { 74 | last_login_date: new Date(), 75 | }; 76 | req.params.id = user.id; 77 | await super.updateById(req, 'Users', data); 78 | const payload = _.omit(user.dataValues, ['createdAt', 'updatedAt', 'last_login_date', 'password', 'gender', 'mobile_number', 'user_image']); 79 | const token = jwt.sign({ payload }, config.auth.jwt_secret, { expiresIn: config.auth.jwt_expiresin, algorithm: 'HS512' }); 80 | const refreshToken = jwt.sign({ 81 | payload, 82 | }, config.auth.refresh_token_secret, { 83 | expiresIn: config.auth.refresh_token_expiresin, 84 | }); 85 | const response = { 86 | status: 'Logged in', 87 | token, 88 | refreshToken, 89 | }; 90 | tokenList[refreshToken] = response; 91 | requestHandler.sendSuccess(res, 'User logged in Successfully')({ token, refreshToken }); 92 | } catch (error) { 93 | requestHandler.sendError(req, res, error); 94 | } 95 | } 96 | 97 | static async signUp(req, res) { 98 | try { 99 | const data = req.body; 100 | const schema = { 101 | email: Joi.string().email().required(), 102 | name: Joi.string().required(), 103 | }; 104 | const randomString = stringUtil.generateString(); 105 | 106 | const { error } = Joi.validate({ email: data.email, name: data.name }, schema); 107 | requestHandler.validateJoi(error, 400, 'bad Request', error ? error.details[0].message : ''); 108 | const options = { where: { email: data.email } }; 109 | const user = await super.getByCustomOptions(req, 'Users', options); 110 | 111 | if (user) { 112 | requestHandler.throwError(400, 'bad request', 'invalid email account,email already existed')(); 113 | } 114 | 115 | async.parallel([ 116 | function one(callback) { 117 | email.sendEmail( 118 | callback, 119 | config.sendgrid.from_email, 120 | [data.email], 121 | ' iLearn Microlearning ', 122 | `please consider the following as your password${randomString}`, 123 | `
Hello ${data.name}
please consider the following as your password: ${randomString}`, 124 | ); 125 | }, 126 | ], (err, results) => { 127 | if (err) { 128 | requestHandler.throwError(500, 'internal Server Error', 'failed to send password email')(); 129 | } else { 130 | logger.log(`an email has been sent at: ${new Date()} to : ${data.email} with the following results ${results}`, 'info'); 131 | } 132 | }); 133 | 134 | const hashedPass = bcrypt.hashSync(randomString, config.auth.saltRounds); 135 | data.password = hashedPass; 136 | const createdUser = await super.create(req, 'Users'); 137 | if (!(_.isNull(createdUser))) { 138 | requestHandler.sendSuccess(res, 'email with your password sent successfully', 201)(); 139 | } else { 140 | requestHandler.throwError(422, 'Unprocessable Entity', 'unable to process the contained instructions')(); 141 | } 142 | } catch (err) { 143 | requestHandler.sendError(req, res, err); 144 | } 145 | } 146 | 147 | static async refreshToken(req, res) { 148 | try { 149 | const data = req.body; 150 | if (_.isNull(data)) { 151 | requestHandler.throwError(400, 'bad request', 'please provide the refresh token in request body')(); 152 | } 153 | const schema = { 154 | refreshToken: Joi.string().required(), 155 | }; 156 | const { error } = Joi.validate({ refreshToken: req.body.refreshToken }, schema); 157 | requestHandler.validateJoi(error, 400, 'bad Request', error ? error.details[0].message : ''); 158 | const tokenFromHeader = auth.getJwtToken(req); 159 | const user = jwt.decode(tokenFromHeader); 160 | 161 | if ((data.refreshToken) && (data.refreshToken in tokenList)) { 162 | const token = jwt.sign({ user }, config.auth.jwt_secret, { expiresIn: config.auth.jwt_expiresin, algorithm: 'HS512' }); 163 | const response = { 164 | token, 165 | }; 166 | // update the token in the list 167 | tokenList[data.refreshToken].token = token; 168 | requestHandler.sendSuccess(res, 'a new token is issued ', 200)(response); 169 | } else { 170 | requestHandler.throwError(400, 'bad request', 'no refresh token present in refresh token list')(); 171 | } 172 | } catch (err) { 173 | requestHandler.sendError(req, res, err); 174 | } 175 | } 176 | 177 | static async logOut(req, res) { 178 | try { 179 | const schema = { 180 | platform: Joi.string().valid('ios', 'android', 'web').required(), 181 | fcmToken: Joi.string(), 182 | }; 183 | const { error } = Joi.validate({ 184 | platform: req.headers.platform, fcmToken: req.body.fcmToken, 185 | }, schema); 186 | requestHandler.validateJoi(error, 400, 'bad Request', error ? error.details[0].message : ''); 187 | 188 | const tokenFromHeader = auth.getJwtToken(req); 189 | const user = jwt.decode(tokenFromHeader); 190 | const options = { 191 | where: { 192 | fcmToken: req.body.fcmToken, 193 | platform: req.headers.platform, 194 | user_id: user.payload.id, 195 | }, 196 | }; 197 | const fmcToken = await super.getByCustomOptions(req, 'UserTokens', options); 198 | req.params.id = fmcToken.dataValues.id; 199 | const deleteFcm = await super.deleteById(req, 'UserTokens'); 200 | if (deleteFcm === 1) { 201 | requestHandler.sendSuccess(res, 'User Logged Out Successfully')(); 202 | } else { 203 | requestHandler.throwError(400, 'bad request', 'User Already logged out Successfully')(); 204 | } 205 | } catch (err) { 206 | requestHandler.sendError(req, res, err); 207 | } 208 | } 209 | } 210 | module.exports = AuthController; 211 | -------------------------------------------------------------------------------- /controllers/BaseController.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const _ = require('lodash'); 4 | const RequestHandler = require('../utils/RequestHandler'); 5 | const Logger = require('../utils/logger'); 6 | 7 | 8 | const logger = new Logger(); 9 | const errHandler = new RequestHandler(logger); 10 | class BaseController { 11 | constructor(options) { 12 | this.limit = 20; 13 | this.options = options; 14 | } 15 | 16 | /** 17 | * Get an element by it's id . 18 | * 19 | * 20 | * @return a Promise 21 | * @return an err if an error occur 22 | */ 23 | static async getById(req, modelName) { 24 | const reqParam = req.params.id; 25 | let result; 26 | try { 27 | result = await req.app.get('db')[modelName].findByPk(reqParam).then( 28 | errHandler.throwIf(r => !r, 404, 'not found', 'Resource not found'), 29 | errHandler.throwError(500, 'sequelize error ,some thing wrong with either the data base connection or schema'), 30 | ); 31 | } catch (err) { 32 | return Promise.reject(err); 33 | } 34 | return result; 35 | } 36 | 37 | static async getByCustomOptions(req, modelName, options) { 38 | let result; 39 | try { 40 | result = await req.app.get('db')[modelName].findOne(options); 41 | } catch (err) { 42 | return Promise.reject(err); 43 | } 44 | return result; 45 | } 46 | 47 | static async deleteById(req, modelName) { 48 | const reqParam = req.params.id; 49 | let result; 50 | try { 51 | result = await req.app.get('db')[modelName].destroy({ 52 | where: { 53 | id: reqParam, 54 | }, 55 | }).then( 56 | errHandler.throwIf(r => r < 1, 404, 'not found', 'No record matches the Id provided'), 57 | errHandler.throwError(500, 'sequelize error'), 58 | ); 59 | } catch (err) { 60 | return Promise.reject(err); 61 | } 62 | return result; 63 | } 64 | 65 | static async create(req, modelName, data) { 66 | let obj = data; 67 | if (_.isUndefined(obj)) { 68 | obj = req.body; 69 | } 70 | let result; 71 | try { 72 | result = await req.app.get('db')[modelName].build(obj).save().then( 73 | errHandler.throwIf(r => !r, 500, 'Internal server error', 'something went wrong couldnt save data'), 74 | errHandler.throwError(500, 'sequelize error'), 75 | 76 | ).then( 77 | savedResource => Promise.resolve(savedResource), 78 | ); 79 | } catch (err) { 80 | return Promise.reject(err); 81 | } 82 | return result; 83 | } 84 | 85 | 86 | static async updateById(req, modelName, data) { 87 | const recordID = req.params.id; 88 | let result; 89 | 90 | try { 91 | result = await req.app.get('db')[modelName] 92 | .update(data, { 93 | where: { 94 | id: recordID, 95 | }, 96 | }).then( 97 | errHandler.throwIf(r => !r, 500, 'Internal server error', 'something went wrong couldnt update data'), 98 | errHandler.throwError(500, 'sequelize error'), 99 | 100 | ).then( 101 | updatedRecored => Promise.resolve(updatedRecored), 102 | ); 103 | } catch (err) { 104 | return Promise.reject(err); 105 | } 106 | return result; 107 | } 108 | 109 | static async updateByCustomWhere(req, modelName, data, options) { 110 | let result; 111 | 112 | try { 113 | result = await req.app.get('db')[modelName] 114 | .update(data, { 115 | where: options, 116 | }).then( 117 | errHandler.throwIf(r => !r, 500, 'Internal server error', 'something went wrong couldnt update data'), 118 | errHandler.throwError(500, 'sequelize error'), 119 | 120 | ).then( 121 | updatedRecored => Promise.resolve(updatedRecored), 122 | ); 123 | } catch (err) { 124 | return Promise.reject(err); 125 | } 126 | return result; 127 | } 128 | 129 | static async getList(req, modelName, options) { 130 | const page = req.query.page; 131 | 132 | let results; 133 | try { 134 | if (_.isUndefined(options)) { 135 | options = {}; 136 | } 137 | 138 | if (parseInt(page, 10)) { 139 | if (page === 0) { 140 | options = _.extend({}, options, {}); 141 | } else { 142 | options = _.extend({}, options, { 143 | offset: this.limit * (page - 1), 144 | limit: this.limit, 145 | }); 146 | } 147 | } else { 148 | options = _.extend({}, options, {}); // extend it so we can't mutate 149 | } 150 | 151 | results = await req.app.get('db')[modelName] 152 | .findAll(options) 153 | .then( 154 | errHandler.throwIf(r => !r, 500, 'Internal server error', 'something went wrong while fetching data'), 155 | errHandler.throwError(500, 'sequelize error'), 156 | ).then(result => Promise.resolve(result)); 157 | } catch (err) { 158 | return Promise.reject(err); 159 | } 160 | return results; 161 | } 162 | } 163 | module.exports = BaseController; 164 | -------------------------------------------------------------------------------- /controllers/UsersController.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const jwt = require('jsonwebtoken'); 3 | const _ = require('lodash'); 4 | const BaseController = require('../controllers/BaseController'); 5 | const RequestHandler = require('../utils/RequestHandler'); 6 | const Logger = require('../utils/logger'); 7 | const auth = require('../utils/auth'); 8 | 9 | const logger = new Logger(); 10 | const requestHandler = new RequestHandler(logger); 11 | 12 | class UsersController extends BaseController { 13 | 14 | static async getUserById(req, res) { 15 | try { 16 | const reqParam = req.params.id; 17 | const schema = { 18 | id: Joi.number().integer().min(1), 19 | }; 20 | const { error } = Joi.validate({ id: reqParam }, schema); 21 | requestHandler.validateJoi(error, 400, 'bad Request', 'invalid User Id'); 22 | 23 | const result = await super.getById(req, 'Users'); 24 | return requestHandler.sendSuccess(res, 'User Data Extracted')({ result }); 25 | } catch (error) { 26 | return requestHandler.sendError(req, res, error); 27 | } 28 | } 29 | 30 | static async deleteById(req, res) { 31 | try { 32 | const result = await super.deleteById(req, 'Users'); 33 | return requestHandler.sendSuccess(res, 'User Deleted Successfully')({ result }); 34 | } catch (err) { 35 | return requestHandler.sendError(req, res, err); 36 | } 37 | } 38 | 39 | static async getProfile(req, res) { 40 | try { 41 | const tokenFromHeader = auth.getJwtToken(req); 42 | const user = jwt.decode(tokenFromHeader); 43 | const options = { 44 | where: { id: user.payload.id }, 45 | }; 46 | const userProfile = await super.getByCustomOptions(req, 'Users', options); 47 | const profile = _.omit(userProfile.dataValues, ['createdAt', 'updatedAt', 'last_login_date', 'password']); 48 | return requestHandler.sendSuccess(res, 'User Profile fetched Successfully')({ profile }); 49 | } catch (err) { 50 | return requestHandler.sendError(req, res, err); 51 | } 52 | } 53 | } 54 | 55 | module.exports = UsersController; 56 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const eslint = require('gulp-eslint'); 3 | const minify = require('gulp-minify'); 4 | const size = require('gulp-size'); 5 | const notify = require('gulp-notify'); 6 | 7 | const paths = { 8 | dist: 'dist', 9 | distIndex: 'dist/index.js', 10 | distJS: 'dist/**/*.js', 11 | }; 12 | 13 | gulp.task('default', ['lint'], () => { 14 | // gulp.watch('js/**/*.js') 15 | }); 16 | 17 | gulp.task('lint', () => gulp.src(['**/*.js', '!node_modules/**']) 18 | .pipe(eslint()) 19 | .pipe(eslint.format()) 20 | .pipe(eslint.failAfterError())); 21 | 22 | gulp.task('compress', () => { 23 | gulp.src(['**/*.js']) 24 | .pipe(minify({ 25 | ignoreFiles: ['node_modules/**', '.env', 'README.md', 'swagger.json'], 26 | })) 27 | .pipe(gulp.dest(paths.dist)).pipe(notify({ 28 | onLast: true, 29 | message: () => 'finished compression', 30 | })); 31 | }); 32 | 33 | gulp.task('sizes', () => gulp.src('**/*.js') 34 | .pipe(size({ showTotal: true, pretty: true }))); 35 | -------------------------------------------------------------------------------- /migrations/20181022121627-create-users.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => queryInterface.createTable('Users', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | name: { 12 | type: Sequelize.STRING, 13 | allowNull: false, 14 | }, 15 | 16 | email: { 17 | type: Sequelize.STRING, 18 | allowNull: false, 19 | }, 20 | gender: { 21 | type: Sequelize.ENUM('male', 'female'), 22 | }, 23 | user_image: { 24 | type: Sequelize.STRING, 25 | allowNull: true, 26 | }, 27 | mobile_number: { 28 | type: Sequelize.STRING, 29 | }, 30 | password: { 31 | type: Sequelize.STRING, 32 | }, 33 | last_login_date: { 34 | type: Sequelize.DATE, 35 | }, 36 | created_at: { 37 | allowNull: false, 38 | type: Sequelize.DATE, 39 | }, 40 | updated_at: { 41 | allowNull: false, 42 | type: Sequelize.DATE, 43 | }, 44 | role_id: { 45 | type: Sequelize.INTEGER, 46 | onDelete: 'NO ACTION', 47 | references: { 48 | model: 'Roles', 49 | key: 'id', 50 | }, 51 | }, 52 | 53 | }), 54 | down: (queryInterface, Sequelize) => queryInterface.dropTable('Users'), 55 | }; 56 | -------------------------------------------------------------------------------- /migrations/20181104092046-create-roles.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => queryInterface.createTable('Roles', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | roleName: { 12 | type: Sequelize.STRING, 13 | }, 14 | created_at: { 15 | allowNull: false, 16 | type: Sequelize.DATE, 17 | }, 18 | updated_at: { 19 | allowNull: false, 20 | type: Sequelize.DATE, 21 | }, 22 | }), 23 | down: (queryInterface, Sequelize) => queryInterface.dropTable('Roles'), 24 | }; 25 | -------------------------------------------------------------------------------- /migrations/20181104092840-create-permissions.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => queryInterface.createTable('Permissions', { 4 | id: { 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | type: Sequelize.INTEGER, 9 | }, 10 | permissionName: { 11 | type: Sequelize.STRING, 12 | }, 13 | created_at: { 14 | allowNull: false, 15 | type: Sequelize.DATE, 16 | }, 17 | updated_at: { 18 | allowNull: false, 19 | type: Sequelize.DATE, 20 | }, 21 | }), 22 | down: (queryInterface, Sequelize) => queryInterface.dropTable('Permissions'), 23 | }; 24 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Sequelize = require('sequelize'); 6 | 7 | const basename = path.basename(__filename); 8 | const env = process.env.NODE_ENV || 'development'; 9 | const config = require(`${__dirname}/../config/config.json`)[env]; 10 | const db = {}; 11 | 12 | let sequelize; 13 | if (config.use_env_variable) { 14 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 15 | } else { 16 | sequelize = new Sequelize(config.database, config.username, config.password, config, { 17 | omitNull: true, 18 | }); 19 | } 20 | 21 | fs 22 | .readdirSync(__dirname) 23 | .filter(file => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')) 24 | .forEach((file) => { 25 | const model = sequelize.import(path.join(__dirname, file)); 26 | db[model.name] = model; 27 | }); 28 | 29 | Object.keys(db).forEach((modelName) => { 30 | if (db[modelName].associate) { 31 | db[modelName].associate(db); 32 | } 33 | }); 34 | 35 | db.sequelize = sequelize; 36 | db.Sequelize = Sequelize; 37 | 38 | module.exports = db; 39 | -------------------------------------------------------------------------------- /models/permissions.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Permissions = sequelize.define('Permissions', { 5 | permissionName: DataTypes.STRING, 6 | }, {}); 7 | Permissions.associate = function (models) { 8 | // associations can be defined here 9 | }; 10 | return Permissions; 11 | }; 12 | -------------------------------------------------------------------------------- /models/roles.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Roles = sequelize.define('Roles', { 5 | roleName: DataTypes.STRING, 6 | }, {}); 7 | Roles.associate = function (models) { 8 | // associations can be defined here 9 | }; 10 | return Roles; 11 | }; 12 | -------------------------------------------------------------------------------- /models/users.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Users = sequelize.define('Users', { 5 | id: { 6 | type: DataTypes.INTEGER(11), 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | }, 11 | name: { 12 | type: DataTypes.STRING, 13 | allowNull: false, 14 | }, 15 | email: { 16 | type: DataTypes.STRING, 17 | allowNull: false, 18 | }, 19 | user_image: { 20 | type: DataTypes.STRING, 21 | allowNull: true, 22 | }, 23 | mobile_number: { 24 | type: DataTypes.STRING, 25 | }, 26 | gender: { 27 | type: DataTypes.ENUM('male', 'female'), 28 | }, 29 | password: { 30 | type: DataTypes.STRING, 31 | }, 32 | last_login_date: { 33 | type: DataTypes.DATE, 34 | }, 35 | createdAt: 36 | { 37 | type: DataTypes.DATE, field: 'created_at', 38 | }, 39 | updatedAt: { 40 | type: DataTypes.DATE, field: 'updated_at', 41 | }, 42 | 43 | }, {}); 44 | Users.associate = function (models) { 45 | Users.hasMany(models.Roles, { 46 | foreignKey: 'role_id', 47 | }); 48 | }; 49 | return Users; 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ilrn", 3 | "version": "1.0.0", 4 | "description": "this is a back end project structure example", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node bin/www.js" 9 | }, 10 | "engines": { 11 | "node": "8.11.0", 12 | "npm": "^5.6.0" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+ssh://github.com/AlaaMezian/NodeJs-backend-structure.git" 17 | }, 18 | "author": "Ala'a Mezian", 19 | "license": "ISC", 20 | "dependencies": { 21 | "async": "^2.6.4", 22 | "bcrypt": "^5.0.0", 23 | "body-parser": "^1.18.3", 24 | "canvas": "^2.0.1", 25 | "compression": "^1.7.3", 26 | "cors": "^2.8.4", 27 | "dotenv": "^6.1.0", 28 | "express": "^4.21.2", 29 | "express-jwt": "^5.3.1", 30 | "extend": "^3.0.2", 31 | "joi": "^14.0.2", 32 | "jsonwebtoken": "^9.0.0", 33 | "lodash": "^4.17.21", 34 | "method-override": "^3.0.0", 35 | "nodemailer": "^6.9.9", 36 | "nodemon": "^3.1.7", 37 | "pg": "^7.5.0", 38 | "pg-hstore": "^2.3.2", 39 | "sendgrid": "^5.2.3", 40 | "sequelize": "^5.15.1", 41 | "sequelize-cli": "^5.2.0", 42 | "swagger-jsdoc": "^3.2.3", 43 | "swagger-model-validator": "^3.0.5", 44 | "swagger-ui-express": "^4.0.1", 45 | "uuid": "^3.3.2", 46 | "winston": "^3.1.0", 47 | "winston-daily-rotate-file": "^3.3.3" 48 | }, 49 | "devDependencies": { 50 | "eslint": "^5.3.0", 51 | "eslint-config-airbnb-base": "^13.1.0", 52 | "eslint-config-prettier": "^3.1.0", 53 | "eslint-plugin-import": "^2.14.0", 54 | "eslint-plugin-node": "^7.0.1", 55 | "eslint-plugin-promise": "^4.0.1", 56 | "gulp": "^3.9.1", 57 | "gulp-eslint": "^5.0.0", 58 | "gulp-minify": "^3.1.0", 59 | "gulp-notify": "^3.2.0", 60 | "gulp-size": "^3.0.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /router/api/authRouter.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const AuthController = require('../../controllers/AuthController'); 3 | const auth = require('../../utils/auth'); 4 | 5 | /** 6 | * @swagger 7 | * definitions: 8 | * users: 9 | * required: 10 | * - id 11 | * - username 12 | * - email 13 | * properties: 14 | * id: 15 | * type: integer 16 | * username: 17 | * type: string 18 | * email: 19 | * type: string 20 | */ 21 | 22 | 23 | /** 24 | * @swagger 25 | * /signUp: 26 | * post: 27 | * tags: 28 | * - Auth 29 | * produces: 30 | * - application/json 31 | * parameters: 32 | * - name: body 33 | * in: body 34 | * description: sign up using email and full name 35 | * required: true 36 | * schema: 37 | * type: object 38 | * required: 39 | * - email 40 | * - name 41 | * properties: 42 | * email: 43 | * type: string 44 | * name: 45 | * type: string 46 | * responses: 47 | * 201: 48 | * description: send an email to the user with the auto generated password and register him 49 | */ 50 | 51 | 52 | router.post('/signUp', AuthController.signUp); 53 | 54 | /** 55 | * @swagger 56 | * /login: 57 | * post: 58 | * tags: 59 | * - Auth 60 | * produces: 61 | * - application/json 62 | * parameters: 63 | * - name: fcmToken 64 | * in: header 65 | * description: fire base cloud messaging token 66 | * required: true 67 | * type: string 68 | * - name: platform 69 | * in: header 70 | * description: the platform that the user is using to access the system ios/android 71 | * required: true 72 | * type: string 73 | * - name: body 74 | * in: body 75 | * description: the login credentials 76 | * required: true 77 | * schema: 78 | * type: object 79 | * required: 80 | * - email 81 | * - password 82 | * properties: 83 | * email: 84 | * type: string 85 | * password: 86 | * type: string 87 | * responses: 88 | * 200: 89 | * description: user logged in succesfully 90 | */ 91 | router.post('/login', AuthController.login); 92 | /** 93 | * @swagger 94 | * /refreshToken: 95 | * post: 96 | * tags: 97 | * - Auth 98 | * security: 99 | * - Bearer: [] 100 | * produces: 101 | * - application/json 102 | * parameters: 103 | * - name: body 104 | * in: body 105 | * description: the refresh token 106 | * required: true 107 | * schema: 108 | * type: object 109 | * required: 110 | * - refreshToken 111 | * properties: 112 | * refreshToken: 113 | * type: string 114 | * responses: 115 | * 200: 116 | * description: a new jwt token with a new expiry date is issued 117 | */ 118 | router.post('/refreshToken', auth.isAuthunticated, AuthController.refreshToken); 119 | 120 | /** 121 | * @swagger 122 | * /logout: 123 | * post: 124 | * tags: 125 | * - Auth 126 | * security: 127 | * - Bearer: [] 128 | * produces: 129 | * - application/json 130 | * parameters: 131 | * - name: platform 132 | * description: device platform 133 | * in: header 134 | * required: true 135 | * type: string 136 | * - name: body 137 | * in: body 138 | * description: the fcm token of the current logged in user 139 | * required: true 140 | * schema: 141 | * type: object 142 | * required: 143 | * - fcmToken 144 | * properties: 145 | * fcmToken: 146 | * type: string 147 | * responses: 148 | * 200: 149 | * description: log out from application 150 | * schema: 151 | * $ref: '#/definitions/users' 152 | */ 153 | router.post('/logout', auth.isAuthunticated, AuthController.logOut); 154 | 155 | module.exports = router; 156 | -------------------------------------------------------------------------------- /router/api/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const router = require('express').Router(); 4 | 5 | router.use('/users', require('./usersRouter')); 6 | 7 | router.use('/email', require('./sendEmail')); 8 | 9 | router.use('/', require('./authRouter')); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /router/api/sendEmail.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const async = require('async'); 3 | const email = require('../../utils/email'); 4 | const config = require('../../config/appconfig'); 5 | const stringUtil = require('../../utils/stringUtil'); 6 | /** 7 | * @swagger 8 | * /email/send: 9 | * post: 10 | * tags: 11 | * - Email 12 | * produces: 13 | * - application/json 14 | * responses: 15 | * 200: 16 | * description: testing send email 17 | */ 18 | 19 | router.post('/send', (req, res) => { 20 | const randomString = stringUtil.generateString(); 21 | async.parallel([ 22 | function one(callback) { 23 | email.sendEmail( 24 | callback, 25 | config.sendgrid.from_email, 26 | ['omarnsoor3@gmail.com'], 27 | ' iLearn Microlearning ', 28 | `please consider the following as your password${randomString}`, 29 | `Welcome to iLrn
please consider the following as your password: ${randomString}`, 30 | ); 31 | }, 32 | ], (err, results) => { 33 | res.send({ 34 | type: 'success', 35 | message: 'Emails sent', 36 | successfulEmails: results[0].successfulEmails === 0 ? 1 : results[0].successfulEmails, 37 | errorEmails: results[0].errorEmails, 38 | }); 39 | }); 40 | }); 41 | 42 | 43 | module.exports = router; 44 | -------------------------------------------------------------------------------- /router/api/usersRouter.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const UsersController = require('../../controllers/UsersController'); 3 | const auth = require('../../utils/auth'); 4 | /** 5 | * @swagger 6 | * definitions: 7 | * users: 8 | * required: 9 | * - id 10 | * - username 11 | * - email 12 | * properties: 13 | * id: 14 | * type: integer 15 | * username: 16 | * type: string 17 | * email: 18 | * type: string 19 | */ 20 | 21 | 22 | /** 23 | * @swagger 24 | * /users/{userId}: 25 | * get: 26 | * tags: 27 | * - users 28 | * description: Return a specific user 29 | * security: 30 | * - Bearer: [] 31 | * produces: 32 | * - application/json 33 | * parameters: 34 | * - name: userId 35 | * description: numeric id of the user to get 36 | * in: path 37 | * required: true 38 | * type: integer 39 | * minimum: 1 40 | * responses: 41 | * 200: 42 | * description: a single user object 43 | * schema: 44 | * $ref: '#/definitions/users' 45 | */ 46 | router.get('/:id([0-9])', auth.isAuthunticated, UsersController.getUserById); 47 | 48 | /** 49 | * @swagger 50 | * /users/{userId}: 51 | * delete: 52 | * tags: 53 | * - users 54 | * security: 55 | * - Bearer: [] 56 | * produces: 57 | * - application/json 58 | * parameters: 59 | * - name: userId 60 | * description: numeric id of the user to get 61 | * in: path 62 | * required: true 63 | * type: integer 64 | * minimum: 1 65 | * responses: 66 | * 200: 67 | * description: delete user with id 68 | * schema: 69 | * $ref: '#/definitions/users' 70 | */ 71 | router.delete('/:id([0-9])', UsersController.deleteById); 72 | 73 | /** 74 | * @swagger 75 | * /users/profile: 76 | * get: 77 | * tags: 78 | * - users 79 | * security: 80 | * - Bearer: [] 81 | * produces: 82 | * - application/json 83 | * responses: 84 | * 200: 85 | * description: return the user profile 86 | * schema: 87 | * $ref: '#/definitions/users' 88 | */ 89 | router.get('/profile', auth.isAuthunticated, UsersController.getProfile); 90 | 91 | 92 | module.exports = router; 93 | -------------------------------------------------------------------------------- /router/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const router = require('express').Router(); 4 | 5 | 6 | router.use('/api/v1', require('./api')); 7 | 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const cors = require('cors'); 4 | const compression = require('compression'); 5 | const uuid = require('uuid'); 6 | const config = require('../config/appconfig'); 7 | const Logger = require('../utils/logger.js'); 8 | 9 | const logger = new Logger(); 10 | const app = express(); 11 | app.set('config', config); // the system configrationsx 12 | app.use(bodyParser.json()); 13 | app.use(require('method-override')()); 14 | 15 | app.use(compression()); 16 | app.use(cors()); 17 | const swagger = require('../utils/swagger'); 18 | 19 | 20 | process.on('SIGINT', () => { 21 | logger.log('stopping the server', 'info'); 22 | process.exit(); 23 | }); 24 | 25 | app.set('db', require('../models/index.js')); 26 | 27 | app.set('port', process.env.DEV_APP_PORT); 28 | app.use('/api/docs', swagger.router); 29 | 30 | app.use((req, res, next) => { 31 | req.identifier = uuid(); 32 | const logString = `a request has been made with the following uuid [${req.identifier}] ${req.url} ${req.headers['user-agent']} ${JSON.stringify(req.body)}`; 33 | logger.log(logString, 'info'); 34 | next(); 35 | }); 36 | 37 | app.use(require('../router')); 38 | 39 | app.use((req, res, next) => { 40 | logger.log('the url you are trying to reach is not hosted on our server', 'error'); 41 | const err = new Error('Not Found'); 42 | err.status = 404; 43 | res.status(err.status).json({ type: 'error', message: 'the url you are trying to reach is not hosted on our server' }); 44 | next(err); 45 | }); 46 | 47 | module.exports = app; 48 | -------------------------------------------------------------------------------- /utils/RequestHandler.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | class RequestHandler { 4 | constructor(logger) { 5 | this.logger = logger; 6 | } 7 | 8 | throwIf(fn, status, errorType, errorMessage) { 9 | return result => (fn(result) ? this.throwError(status, errorType, errorMessage)() : result); 10 | } 11 | 12 | validateJoi(err, status, errorType, errorMessage) { 13 | if (err) { this.logger.log(`error in validating request : ${errorMessage}`, 'warn'); } 14 | return !_.isNull(err) ? this.throwError(status, errorType, errorMessage)() : ''; 15 | } 16 | 17 | throwError(status, errorType, errorMessage) { 18 | return (e) => { 19 | if (!e) e = new Error(errorMessage || 'Default Error'); 20 | e.status = status; 21 | e.errorType = errorType; 22 | throw e; 23 | }; 24 | } 25 | 26 | catchError(res, error) { 27 | if (!error) error = new Error('Default error'); 28 | res.status(error.status || 500).json({ type: 'error', message: error.message || 'Unhandled error', error }); 29 | } 30 | 31 | sendSuccess(res, message, status) { 32 | this.logger.log(`a request has been made and proccessed successfully at: ${new Date()}`, 'info'); 33 | return (data, globalData) => { 34 | if (_.isUndefined(status)) { 35 | status = 200; 36 | } 37 | res.status(status).json({ 38 | type: 'success', message: message || 'Success result', data, ...globalData, 39 | }); 40 | }; 41 | } 42 | 43 | sendError(req, res, error) { 44 | this.logger.log(`error ,Error during processing request: ${`${req.protocol}://${req.get('host')}${req.originalUrl}`} details message: ${error.message}`, 'error'); 45 | return res.status(error.status || 500).json({ 46 | type: 'error', message: error.message || error.message || 'Unhandled Error', error, 47 | }); 48 | } 49 | } 50 | module.exports = RequestHandler; 51 | -------------------------------------------------------------------------------- /utils/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const _ = require('lodash'); 3 | const config = require('../config/appconfig'); 4 | const RequestHandler = require('../utils/RequestHandler'); 5 | const Logger = require('../utils/logger'); 6 | 7 | const logger = new Logger(); 8 | const requestHandler = new RequestHandler(logger); 9 | function getTokenFromHeader(req) { 10 | if ((req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Token') 11 | || (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer')) { 12 | return req.headers.authorization.split(' ')[1]; 13 | } 14 | 15 | return null; 16 | } 17 | 18 | function verifyToken(req, res, next) { 19 | try { 20 | if (_.isUndefined(req.headers.authorization)) { 21 | requestHandler.throwError(401, 'Unauthorized', 'Not Authorized to access this resource!')(); 22 | } 23 | const Bearer = req.headers.authorization.split(' ')[0]; 24 | 25 | if (!Bearer || Bearer !== 'Bearer') { 26 | requestHandler.throwError(401, 'Unauthorized', 'Not Authorized to access this resource!')(); 27 | } 28 | 29 | const token = req.headers.authorization.split(' ')[1]; 30 | 31 | if (!token) { 32 | requestHandler.throwError(401, 'Unauthorized', 'Not Authorized to access this resource!')(); 33 | } 34 | 35 | // verifies secret and checks exp 36 | jwt.verify(token, config.auth.jwt_secret, (err, decoded) => { 37 | if (err) { 38 | requestHandler.throwError(401, 'Unauthorized', 'please provide a vaid token ,your token might be expired')(); 39 | } 40 | req.decoded = decoded; 41 | next(); 42 | }); 43 | } catch (err) { 44 | requestHandler.sendError(req, res, err); 45 | } 46 | } 47 | 48 | 49 | module.exports = { getJwtToken: getTokenFromHeader, isAuthunticated: verifyToken }; 50 | -------------------------------------------------------------------------------- /utils/email.js: -------------------------------------------------------------------------------- 1 | const helper = require('sendgrid').mail; 2 | const async = require('async'); 3 | const config = require('../config/appconfig'); 4 | const sg = require('sendgrid')(config.sendgrid.api_key); 5 | const Logger = require('../utils/logger'); 6 | 7 | const logger = new Logger(); 8 | 9 | module.exports = { 10 | 11 | sendEmail( 12 | parentCallback, 13 | fromEmail, 14 | toEmails, 15 | subject, 16 | textContent, 17 | htmlContent, 18 | ) { 19 | const errorEmails = []; 20 | const successfulEmails = []; 21 | async.parallel([ 22 | function one(callback) { 23 | // Add to emails 24 | for (let i = 0; i < toEmails.length; i += 1) { 25 | // Add from emails 26 | const senderEmail = new helper.Email(fromEmail); 27 | // Add to email 28 | const toEmail = new helper.Email(toEmails[i]); 29 | // HTML Content 30 | const content = new helper.Content('text/html', htmlContent); 31 | const mail = new helper.Mail(senderEmail, subject, toEmail, content); 32 | const request = sg.emptyRequest({ 33 | method: 'POST', 34 | path: '/v3/mail/send', 35 | body: mail.toJSON(), 36 | }); 37 | sg.API(request, (error, response) => { 38 | if (error) { 39 | logger.log(`error ,Error during processing request at : ${new Date()} details message: ${error.message}`, 'error'); 40 | } 41 | console.log(response.statusCode); 42 | console.log(response.body); 43 | console.log(response.headers); 44 | }); 45 | } 46 | // return 47 | callback(null, true); 48 | }, 49 | ], (err, results) => { 50 | if (err) { 51 | logger.log(`error ,Error during processing request at : ${new Date()} details message: ${err.message}`, 'error'); 52 | } else { 53 | logger.log(`an email has been sent: ${new Date()} with results: ${results}`, 'info'); 54 | } 55 | console.log('Done'); 56 | }); 57 | parentCallback(null, 58 | { 59 | successfulEmails, 60 | errorEmails, 61 | }); 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /utils/errors.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const { extend } = require('lodash').extend; 4 | 5 | module.exports = { 6 | BadRequest: { 7 | error: 'Bad Request', 8 | status: 400, 9 | }, 10 | 11 | Unauthorized: { 12 | error: 'Unauthorised', 13 | status: 401, 14 | }, 15 | 16 | Forbidden: { 17 | error: 'Forbidden', 18 | status: 403, 19 | }, 20 | 21 | NotFound: { 22 | error: 'Not Found', 23 | status: 404, 24 | }, 25 | 26 | UnprocessableEntity: { 27 | status: 422, 28 | error: 'Unprocessable Entity', 29 | }, 30 | 31 | InternalServerError: { 32 | error: 'Internal Server Error', 33 | status: 500, 34 | }, 35 | 36 | Success: { 37 | error: '', 38 | status: 200, 39 | }, 40 | 41 | onlyAdmin: extend({}, this.Forbidden, { 42 | message: 'Only admins are allowed to do this!', 43 | }), 44 | 45 | NoPermesssion: extend({}, { 46 | error: 'Forbidden', 47 | status: 403, 48 | message: 'You do not have permission to consume this resource!', 49 | }), 50 | 51 | invalidId: extend({}, this.BadRequest, { 52 | message: 'Invalid Id parameter', 53 | }), 54 | 55 | invalidSearchTerm: extend({}, this.BadRequest, { 56 | message: 'Invalid search term', 57 | }), 58 | 59 | missingAttr(attrs) { 60 | return extend({}, this.BadRequest, { 61 | message: `Attribute(s) (${attrs.join(',')}) seem(s) to be missing`, 62 | }); 63 | }, 64 | 65 | unwantedAttr(attrs) { 66 | return extend({}, this.BadRequest, { 67 | message: `Attribute(s) (${attrs.join(',')}) can't be updated`, 68 | }); 69 | }, 70 | 71 | uniqueAttr(attrs) { 72 | return extend({}, this.BadRequest, { 73 | message: `Attribute(s) [${attrs.join(',')}] must be unique`, 74 | }); 75 | }, 76 | 77 | custom(msg) { 78 | return extend({}, this.BadRequest, { 79 | message: msg, 80 | }); 81 | }, 82 | 83 | // REST 84 | 85 | addFailure() { 86 | return extend({}, this.BadRequest, { 87 | message: 'Item WAS NOT added', 88 | }); 89 | }, 90 | 91 | deleteFailure() { 92 | return extend({}, this.BadRequest, { 93 | message: 'Item WAS NOT deleted', 94 | }); 95 | }, 96 | 97 | updateFailure() { 98 | return extend({}, this.BadRequest, { 99 | message: 'Item WAS NOT updated', 100 | }); 101 | }, 102 | 103 | addSuccess() { 104 | return extend({}, this.Success, { 105 | message: 'Item added successfully', 106 | }); 107 | }, 108 | 109 | deleteSuccess() { 110 | return extend({}, this.Success, { 111 | message: 'Item deleted successfully', 112 | }); 113 | }, 114 | 115 | updateSuccess() { 116 | return extend({}, this.Success, { 117 | message: 'Item updated successfully', 118 | }); 119 | }, 120 | 121 | empty: [], 122 | }; 123 | -------------------------------------------------------------------------------- /utils/logger.js: -------------------------------------------------------------------------------- 1 | 2 | const { createLogger, format, transports } = require('winston'); 3 | const fs = require('fs'); 4 | const DailyRotate = require('winston-daily-rotate-file'); 5 | const config = require('../config/appconfig'); 6 | 7 | const { env } = config.app; 8 | const logDir = 'log'; 9 | 10 | let infoLogger; 11 | let errorLogger; 12 | let warnLogger; 13 | let allLogger; 14 | 15 | class Logger { 16 | constructor() { 17 | if (!fs.existsSync(logDir)) { 18 | fs.mkdirSync(logDir); 19 | } 20 | 21 | infoLogger = createLogger({ 22 | // change level if in dev environment versus production 23 | level: env === 'development' ? 'info' : 'debug', 24 | format: format.combine( 25 | format.timestamp({ 26 | format: 'YYYY-MM-DD HH:mm:ss', 27 | }), 28 | format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`), 29 | // this is to log in json format 30 | // format.json() 31 | 32 | ), 33 | transports: [ 34 | new transports.Console({ 35 | levels: 'info', 36 | format: format.combine( 37 | format.colorize(), 38 | format.printf( 39 | info => `${info.timestamp} ${info.level}: ${info.message}`, 40 | ), 41 | ), 42 | }), 43 | 44 | new (DailyRotate)({ 45 | filename: `${logDir}/%DATE%-info-results.log`, 46 | datePattern: 'YYYY-MM-DD', 47 | }), 48 | ], 49 | exitOnError: false, 50 | }); 51 | 52 | errorLogger = createLogger({ 53 | // change level if in dev environment versus production 54 | format: format.combine( 55 | format.timestamp({ 56 | format: 'YYYY-MM-DD HH:mm:ss', 57 | }), 58 | format.printf(error => `${error.timestamp} ${error.level}: ${error.message}`), 59 | 60 | ), 61 | transports: [ 62 | new transports.Console({ 63 | levels: 'error', 64 | format: format.combine( 65 | format.colorize(), 66 | format.printf( 67 | error => `${error.timestamp} ${error.level}: ${error.message}`, 68 | ), 69 | ), 70 | }), 71 | 72 | new (DailyRotate)({ 73 | filename: `${logDir}/%DATE%-errors-results.log`, 74 | datePattern: 'YYYY-MM-DD', 75 | }), 76 | ], 77 | exitOnError: false, 78 | }); 79 | 80 | warnLogger = createLogger({ 81 | // change level if in dev environment versus production 82 | format: format.combine( 83 | format.timestamp({ 84 | format: 'YYYY-MM-DD HH:mm:ss', 85 | }), 86 | format.printf(warn => `${warn.timestamp} ${warn.level}: ${warn.message}`), 87 | 88 | ), 89 | transports: [ 90 | new transports.Console({ 91 | levels: 'warn', 92 | format: format.combine( 93 | format.colorize(), 94 | format.printf( 95 | warn => `${warn.timestamp} ${warn.level}: ${warn.message}`, 96 | ), 97 | ), 98 | }), 99 | 100 | new (DailyRotate)({ 101 | filename: `${logDir}/%DATE%-warnings-results.log`, 102 | datePattern: 'YYYY-MM-DD', 103 | }), 104 | ], 105 | exitOnError: false, 106 | }); 107 | 108 | allLogger = createLogger({ 109 | // change level if in dev environment versus production 110 | format: format.combine( 111 | format.timestamp({ 112 | format: 'YYYY-MM-DD HH:mm:ss', 113 | }), 114 | format.printf(silly => `${silly.timestamp} ${silly.level}: ${silly.message}`), 115 | 116 | ), 117 | transports: [ 118 | new (DailyRotate)({ 119 | filename: `${logDir}/%DATE%-results.log`, 120 | datePattern: 'YYYY-MM-DD', 121 | }), 122 | ], 123 | exitOnError: false, 124 | }); 125 | } 126 | 127 | log(message, severity, data) { 128 | if (severity == null || infoLogger.levels[severity] == null) { 129 | this.severity = 'info'; 130 | } 131 | if (severity === 'info') { 132 | infoLogger.log(severity, message, data); 133 | allLogger.log(severity, message, data); 134 | } else if (severity === 'error') { 135 | errorLogger.log(severity, message); 136 | allLogger.log(severity, message, data); 137 | } else if (severity === 'warn') { 138 | warnLogger.log(severity, message, data); 139 | allLogger.log(severity, message, data); 140 | } 141 | } 142 | } 143 | 144 | module.exports = Logger; 145 | -------------------------------------------------------------------------------- /utils/stringUtil.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // generate random string 3 | generateString() { 4 | let text = ''; 5 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 6 | 7 | for (let i = 0; i < 6; i += 1) { 8 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 9 | } 10 | return text; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /utils/swagger.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const router = express.Router(); 4 | 5 | const swaggerJSDoc = require('swagger-jsdoc'); 6 | const swaggerUi = require('swagger-ui-express'); 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const _ = require('lodash'); 11 | const config = require('../config/appconfig'); 12 | 13 | const directoryPath = path.join(__dirname, '../router/api'); 14 | const pathes = []; 15 | 16 | const filesName = fs.readdirSync(directoryPath, (err, files) => { 17 | // handling error 18 | if (err) { 19 | return console.log(`Unable to scan directory: ${err}`); 20 | } 21 | // listing all files using forEach 22 | return files.forEach(file => pathes.push(file)); 23 | }); 24 | function getFullPathes(names) { 25 | names.forEach((name) => { 26 | let customePath; 27 | if (name !== 'index') { 28 | customePath = `./router/api/${name}`; 29 | } 30 | if (!(_.isUndefined(name))) { 31 | pathes.push(customePath); 32 | } 33 | }); 34 | } 35 | 36 | getFullPathes(filesName); 37 | 38 | const options = { 39 | swaggerDefinition: { 40 | info: { 41 | title: 'i Lrn', 42 | version: '1.0.0', 43 | description: 'i Lrn Microlearning System,REST API with Swagger doc', 44 | contact: { 45 | email: 'a.mezian@dreamtechs.co', 46 | }, 47 | }, 48 | tags: [ 49 | { 50 | name: 'users', 51 | description: 'Users API', 52 | }, 53 | { 54 | name: 'Auth', 55 | description: 'Authentication apis', 56 | }, 57 | { 58 | name: 'Email', 59 | description: 'for testing and sending emails ', 60 | }, 61 | { 62 | name: 'termsAndCondition', 63 | description: ' the terms and condition for the application', 64 | 65 | }, 66 | { 67 | name: 'Versioning', 68 | description: ' operation related to check the version of the apis or the mobile .. etc ', 69 | 70 | }, 71 | ], 72 | schemes: ['http'], 73 | host: `localhost:${config.app.port}`, 74 | basePath: '/api/v1', 75 | securityDefinitions: { 76 | Bearer: { 77 | type: 'apiKey', 78 | description: 'JWT authorization of an API', 79 | name: 'Authorization', 80 | in: 'header', 81 | }, 82 | }, 83 | }, 84 | 85 | apis: pathes, 86 | }; 87 | const swaggerSpec = swaggerJSDoc(options); 88 | require('swagger-model-validator')(swaggerSpec); 89 | 90 | router.get('/json', (req, res) => { 91 | res.setHeader('Content-Type', 'application/json'); 92 | res.send(swaggerSpec); 93 | }); 94 | 95 | router.use('/', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); 96 | 97 | function validateModel(name, model) { 98 | const responseValidation = swaggerSpec.validateModel(name, model, false, true); 99 | if (!responseValidation.valid) { 100 | throw new Error('Model doesn\'t match Swagger contract'); 101 | } 102 | } 103 | 104 | module.exports = { 105 | router, 106 | validateModel, 107 | }; 108 | --------------------------------------------------------------------------------