├── .babelrc ├── .env.example ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .prettierrc ├── LICENSE ├── Procfile ├── README.md ├── ecosystem.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── public ├── index.html └── libeyondea.png └── src ├── app.js ├── config ├── config.js ├── initialData.js ├── logger.js ├── mongoose.js ├── morgan.js └── passport.js ├── controllers ├── authController.js ├── imageController.js ├── roleController.js └── userController.js ├── index.js ├── middlewares ├── authenticate.js ├── error.js ├── rateLimiter.js ├── uploadImage.js └── validate.js ├── models ├── permissionModel.js ├── plugins │ ├── paginatePlugin.js │ └── toJSONPlugin.js ├── roleModel.js ├── tokenModel.js └── userModel.js ├── routes └── v1 │ ├── authRoute.js │ ├── imageRoute.js │ ├── index.js │ ├── roleRoute.js │ └── userRoute.js ├── services ├── emailService │ ├── index.js │ └── template.js ├── jwtService.js └── tokenService.js ├── utils ├── apiError.js ├── catchAsync.js └── resizeImage.js └── validations ├── authValidation.js ├── customValidation.js ├── roleValidation.js └── userValidation.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | [ 7 | "babel-plugin-root-import", 8 | { 9 | "paths": [ 10 | { 11 | "rootPathPrefix": "~/", 12 | "rootPathSuffix": "src" 13 | } 14 | ] 15 | } 16 | ], 17 | "@babel/plugin-transform-runtime" 18 | ] 19 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME = 2 | 3 | HOST = 4 | PORT = 5 | 6 | DATABASE_URI = mongodb://127.0.0.1:27017/database_name 7 | 8 | JWT_ACCESS_TOKEN_SECRET_PRIVATE = 9 | JWT_ACCESS_TOKEN_SECRET_PUBLIC = 10 | JWT_ACCESS_TOKEN_EXPIRATION_MINUTES = 11 | 12 | REFRESH_TOKEN_EXPIRATION_DAYS = 13 | VERIFY_EMAIL_TOKEN_EXPIRATION_MINUTES = 14 | RESET_PASSWORD_TOKEN_EXPIRATION_MINUTES = 15 | 16 | SMTP_HOST = smtp.googlemail.com 17 | SMTP_PORT = 465 18 | SMTP_USERNAME = 19 | SMTP_PASSWORD = 20 | EMAIL_FROM = 21 | 22 | FRONTEND_URL = 23 | 24 | IMAGE_URL = -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 7 | "plugins": [], 8 | "parserOptions": { 9 | "ecmaVersion": 2018 10 | }, 11 | "parser": "@babel/eslint-parser", 12 | "rules": { 13 | "prettier/prettier": ["error"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # misc 5 | .env 6 | 7 | # build 8 | /dist 9 | 10 | #log 11 | /logs 12 | 13 | # images 14 | /public/images 15 | 16 | # storage 17 | /storage/*.key* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "useTabs": true, 6 | "printWidth": 130 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 libeyondea 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run prod -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RESTful API Node Express Mongoose Example 2 | 3 | The project builds RESTful APIs using Node.js, Express and Mongoose, ... 4 | 5 | ## Manual Installation 6 | 7 | Clone the repo: 8 | 9 | ```bash 10 | git clone https://github.com/libeyondea/backend-node-express.git 11 | cd backend-node-express 12 | ``` 13 | 14 | Install the dependencies: 15 | 16 | ```bash 17 | npm install 18 | ``` 19 | 20 | Set the environment variables: 21 | 22 | ```bash 23 | cp .env.example .env 24 | # open .env and modify the environment variables 25 | ``` 26 | 27 | Generate JWT RS256 key: 28 | 29 | ```bash 30 | ssh-keygen -t rsa -P "" -b 2048 -m PEM -f storage/jwtRS256.key 31 | ssh-keygen -e -m PEM -f storage/jwtRS256.key > storage/jwtRS256.key.pub 32 | # encode base64 33 | cat storage/jwtRS256.key | base64 # edit JWT_ACCESS_TOKEN_SECRET_PRIVATE in .env 34 | cat storage/jwtRS256.key.pub | base64 # edit JWT_ACCESS_TOKEN_SECRET_PUBLIC in .env 35 | ``` 36 | 37 | ## Table of Contents 38 | 39 | - [Commands](#commands) 40 | - [Environment Variables](#environment-variables) 41 | - [Project Structure](#project-structure) 42 | - [API Endpoints](#api-endpoints) 43 | 44 | ## Commands 45 | 46 | Running in development: 47 | 48 | ```bash 49 | npm start 50 | # or 51 | npm run dev 52 | ``` 53 | 54 | Running in production: 55 | 56 | ```bash 57 | # build 58 | npm run build 59 | # start 60 | npm run prod 61 | ``` 62 | 63 | ## Environment Variables 64 | 65 | The environment variables can be found and modified in the `.env` file. 66 | 67 | ```bash 68 | # App name 69 | APP_NAME = # default App Name 70 | 71 | # Host 72 | HOST = # default 0.0.0.0 73 | # Port 74 | PORT = # default 666 75 | 76 | # URL of the Mongo DB 77 | DATABASE_URI = mongodb://127.0.0.1:27017/database_name 78 | 79 | # JWT 80 | JWT_ACCESS_TOKEN_SECRET_PRIVATE = 81 | JWT_ACCESS_TOKEN_SECRET_PUBLIC = 82 | JWT_ACCESS_TOKEN_EXPIRATION_MINUTES = # default 240 minutes 83 | 84 | # Token expires 85 | REFRESH_TOKEN_EXPIRATION_DAYS = # default 1 day 86 | VERIFY_EMAIL_TOKEN_EXPIRATION_MINUTES = # default 60 minutes 87 | RESET_PASSWORD_TOKEN_EXPIRATION_MINUTES = # default 30 minutes 88 | 89 | # SMTP configuration 90 | SMTP_HOST = smtp.googlemail.com 91 | SMTP_PORT = 465 92 | SMTP_USERNAME = 93 | SMTP_PASSWORD = 94 | EMAIL_FROM = 95 | 96 | # URL frontend 97 | FRONTEND_URL = # default http://localhost:777 98 | 99 | # URL images 100 | IMAGE_URL = # default http://localhost:666/images 101 | ``` 102 | 103 | ## Project Structure 104 | 105 | ``` 106 | public\ # Public folder 107 | |--index.html # Static html 108 | src\ 109 | |--config\ # Environment variables and configuration 110 | |--controllers\ # Controllers 111 | |--middlewares\ # Custom express middlewares 112 | |--models\ # Mongoose models 113 | |--routes\ # Routes 114 | |--services\ # Business logic 115 | |--utils\ # Utility classes and functions 116 | |--validations\ # Request data validation schemas 117 | |--index.js # App entry point 118 | ``` 119 | 120 | ### API Endpoints 121 | 122 | List of available routes: 123 | 124 | **Auth routes**:\ 125 | `POST api/v1/auth/signup` - Signup\ 126 | `POST api/v1/auth/signin` - Signin\ 127 | `POST api/v1/auth/logout` - Logout\ 128 | `POST api/v1/auth/refresh-tokens` - Refresh auth tokens\ 129 | `POST api/v1/auth/forgot-password` - Send reset password email\ 130 | `POST api/v1/auth/reset-password` - Reset password\ 131 | `POST api/v1/auth/send-verification-email` - Send verification email\ 132 | `POST api/v1/auth/verify-email` - Verify email\ 133 | `POST api/v1/auth/me` - Profile\ 134 | `PUT api/v1/auth/me` - Update profile 135 | 136 | **User routes**:\ 137 | `POST api/v1/users` - Create a user\ 138 | `GET api/v1/users` - Get all users\ 139 | `GET api/v1/users/:userId` - Get user\ 140 | `PUT api/v1/users/:userId` - Update user\ 141 | `DELETE api/v1/users/:userId` - Delete user 142 | 143 | **Role routes**:\ 144 | `POST api/v1/roles` - Create a role\ 145 | `GET api/v1/roles` - Get all roles\ 146 | `GET api/v1/roles/:userId` - Get role\ 147 | `PUT api/v1/roles/:userId` - Update role\ 148 | `DELETE api/v1/roles/:userId` - Delete role 149 | 150 | **Image routes**:\ 151 | `POST api/v1/images/upload` - Upload image 152 | 153 | ## License 154 | 155 | [MIT](LICENSE) 156 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'backend-node-express"', 5 | script: 'dist/index.js', 6 | instances: 'max', 7 | env: { 8 | NODE_ENV: 'development' 9 | }, 10 | env_production: { 11 | NODE_ENV: 'production' 12 | } 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": [ 6 | "src/*" 7 | ] 8 | } 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ] 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend-node-express", 3 | "version": "1.5.2", 4 | "private": true, 5 | "scripts": { 6 | "start": "npm run dev", 7 | "dev": "cross-env NODE_ENV=development nodemon src --exec babel-node", 8 | "build": "babel src -d dist", 9 | "prod": "pm2-runtime start ecosystem.config.js --env production" 10 | }, 11 | "dependencies": { 12 | "@babel/runtime": "^7.15.3", 13 | "bcryptjs": "^2.4.3", 14 | "compression": "^1.7.4", 15 | "cors": "^2.8.5", 16 | "cross-env": "^7.0.3", 17 | "dotenv": "^10.0.0", 18 | "express": "^4.17.1", 19 | "express-rate-limit": "^5.3.0", 20 | "helmet": "^4.6.0", 21 | "http-status": "^1.5.0", 22 | "joi": "^17.4.2", 23 | "jsonwebtoken": "^8.5.1", 24 | "lodash": "^4.17.21", 25 | "moment": "^2.29.1", 26 | "mongoose": "^6.0.0", 27 | "morgan": "^1.10.0", 28 | "multer": "^1.4.3", 29 | "nodemailer": "^6.6.3", 30 | "passport": "^0.4.1", 31 | "passport-jwt": "^4.0.0", 32 | "pm2": "^5.1.1", 33 | "sharp": "^0.29.1", 34 | "uuid": "^8.3.2", 35 | "validator": "^13.6.0", 36 | "winston": "^3.3.3" 37 | }, 38 | "devDependencies": { 39 | "@babel/cli": "^7.14.8", 40 | "@babel/core": "^7.15.0", 41 | "@babel/eslint-parser": "^7.15.4", 42 | "@babel/node": "^7.14.9", 43 | "@babel/plugin-transform-runtime": "^7.15.0", 44 | "@babel/preset-env": "^7.15.0", 45 | "babel-plugin-root-import": "^6.6.0", 46 | "eslint": "^7.32.0", 47 | "eslint-config-airbnb-base": "^14.2.1", 48 | "eslint-config-prettier": "^8.3.0", 49 | "eslint-plugin-import": "^2.24.2", 50 | "eslint-plugin-prettier": "^4.0.0", 51 | "nodemon": "^2.0.12", 52 | "prettier": "^2.4.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | De4th Zone 6 | 17 | 18 | 19 | 20 |
21 |

██████╗░░░██╗██╗███████╗

22 |

██╔══██╗░██╔╝██║╚════██║

23 |

██║░░██║██╔╝░██║░░███╔═╝

24 |

██║░░██║███████║██╔══╝░░

25 |

██████╔╝╚════██║███████╗

26 |

╚═════╝░░░░░░╚═╝╚══════╝

27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /public/libeyondea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libeyondea/backend-node-express/eb27d12c06b175e8a7cfd354547953295840751e/public/libeyondea.png -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import compression from 'compression'; 3 | import helmet from 'helmet'; 4 | import cors from 'cors'; 5 | import passport from '~/config/passport'; 6 | import routes from '~/routes/v1'; 7 | import error from '~/middlewares/error'; 8 | import rateLimiter from '~/middlewares/rateLimiter'; 9 | import config from '~/config/config'; 10 | import morgan from '~/config/morgan'; 11 | 12 | const app = express(); 13 | 14 | if (config.NODE_ENV !== 'test') { 15 | app.use(morgan); 16 | } 17 | 18 | app.use(helmet()); 19 | app.use(express.json()); 20 | app.use(express.urlencoded({ extended: true })); 21 | app.use(compression()); 22 | app.use(cors()); 23 | app.use(rateLimiter); 24 | app.use(passport.initialize()); 25 | app.use(express.static('public')); 26 | app.use('/api/v1', routes); 27 | app.use(error.converter); 28 | app.use(error.notFound); 29 | app.use(error.handler); 30 | 31 | export default app; 32 | -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import Joi from 'joi'; 3 | 4 | dotenv.config(); 5 | 6 | const envValidate = Joi.object() 7 | .keys({ 8 | NODE_ENV: Joi.string().valid('production', 'development', 'test').required(), 9 | APP_NAME: Joi.string().allow('').empty('').default('App Name'), 10 | HOST: Joi.string().allow('').empty('').default('0.0.0.0'), 11 | PORT: Joi.number().allow('').empty('').default(666), 12 | 13 | DATABASE_URI: Joi.string().required(), 14 | 15 | JWT_ACCESS_TOKEN_SECRET_PRIVATE: Joi.string().required(), 16 | JWT_ACCESS_TOKEN_SECRET_PUBLIC: Joi.string().required(), 17 | JWT_ACCESS_TOKEN_EXPIRATION_MINUTES: Joi.number().allow('').empty('').default(240), 18 | 19 | REFRESH_TOKEN_EXPIRATION_DAYS: Joi.number().allow('').empty('').default(1), 20 | VERIFY_EMAIL_TOKEN_EXPIRATION_MINUTES: Joi.number().allow('').empty('').default(60), 21 | RESET_PASSWORD_TOKEN_EXPIRATION_MINUTES: Joi.number().allow('').empty('').default(30), 22 | 23 | SMTP_HOST: Joi.string().allow('').empty(''), 24 | SMTP_PORT: Joi.number().allow('').empty(''), 25 | SMTP_USERNAME: Joi.string().allow('').empty(''), 26 | SMTP_PASSWORD: Joi.string().allow('').empty(''), 27 | EMAIL_FROM: Joi.string().allow('').empty(''), 28 | 29 | FRONTEND_URL: Joi.string().allow('').empty('').default('http://localhost:777'), 30 | IMAGE_URL: Joi.string().allow('').empty('').default('http://localhost:666/images') 31 | }) 32 | .unknown(); 33 | 34 | const { value: env, error } = envValidate.prefs({ errors: { label: 'key' } }).validate(process.env); 35 | 36 | if (error) { 37 | throw new Error(`Config env error: ${error.message}`); 38 | } 39 | 40 | export default { 41 | NODE_ENV: env.NODE_ENV, 42 | APP_NAME: env.APP_NAME, 43 | HOST: env.HOST, 44 | PORT: env.PORT, 45 | 46 | DATABASE_URI: env.DATABASE_URI, 47 | DATABASE_OPTIONS: { 48 | useNewUrlParser: true, 49 | useUnifiedTopology: true, 50 | retryWrites: true, 51 | w: 'majority' 52 | }, 53 | 54 | JWT_ACCESS_TOKEN_SECRET_PRIVATE: Buffer.from(env.JWT_ACCESS_TOKEN_SECRET_PRIVATE, 'base64'), 55 | JWT_ACCESS_TOKEN_SECRET_PUBLIC: Buffer.from(env.JWT_ACCESS_TOKEN_SECRET_PUBLIC, 'base64'), 56 | JWT_ACCESS_TOKEN_EXPIRATION_MINUTES: env.JWT_ACCESS_TOKEN_EXPIRATION_MINUTES, 57 | 58 | REFRESH_TOKEN_EXPIRATION_DAYS: env.REFRESH_TOKEN_EXPIRATION_DAYS, 59 | VERIFY_EMAIL_TOKEN_EXPIRATION_MINUTES: env.VERIFY_EMAIL_TOKEN_EXPIRATION_MINUTES, 60 | RESET_PASSWORD_TOKEN_EXPIRATION_MINUTES: env.RESET_PASSWORD_TOKEN_EXPIRATION_MINUTES, 61 | 62 | SMTP_HOST: env.SMTP_HOST, 63 | SMTP_PORT: env.SMTP_PORT, 64 | SMTP_USERNAME: env.SMTP_USERNAME, 65 | SMTP_PASSWORD: env.SMTP_PASSWORD, 66 | EMAIL_FROM: env.EMAIL_FROM, 67 | 68 | FRONTEND_URL: env.FRONTEND_URL, 69 | 70 | IMAGE_URL: env.IMAGE_URL, 71 | 72 | TOKEN_TYPES: { 73 | REFRESH: 'refresh', 74 | VERIFY_EMAIL: 'verifyEmail', 75 | RESET_PASSWORD: 'resetPassword' 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/config/initialData.js: -------------------------------------------------------------------------------- 1 | import Permission from '~/models/permissionModel'; 2 | import Role from '~/models/roleModel'; 3 | import User from '~/models/userModel'; 4 | import logger from './logger'; 5 | 6 | async function initialData() { 7 | try { 8 | const countPermissions = await Permission.estimatedDocumentCount(); 9 | if (countPermissions === 0) { 10 | await Permission.create( 11 | { 12 | controller: 'user', 13 | action: 'create' 14 | }, 15 | { 16 | controller: 'user', 17 | action: 'read' 18 | }, 19 | { 20 | controller: 'user', 21 | action: 'update' 22 | }, 23 | { 24 | controller: 'user', 25 | action: 'delete' 26 | }, 27 | { 28 | controller: 'role', 29 | action: 'create' 30 | }, 31 | { 32 | controller: 'role', 33 | action: 'read' 34 | }, 35 | { 36 | controller: 'role', 37 | action: 'update' 38 | }, 39 | { 40 | controller: 'role', 41 | action: 'delete' 42 | } 43 | ); 44 | } 45 | const countRoles = await Role.estimatedDocumentCount(); 46 | if (countRoles === 0) { 47 | const permissionsSuperAdministrator = await Permission.find(); 48 | const permissionsAdministrator = await Permission.find({ controller: 'user' }); 49 | const permissionsModerator = await Permission.find({ controller: 'user', action: { $ne: 'delete' } }); 50 | await Role.create( 51 | { 52 | name: 'Super Administrator', 53 | permissions: permissionsSuperAdministrator 54 | }, 55 | { 56 | name: 'Administrator', 57 | permissions: permissionsAdministrator 58 | }, 59 | { 60 | name: 'Moderator', 61 | permissions: permissionsModerator 62 | }, 63 | { 64 | name: 'User', 65 | permissions: [] 66 | } 67 | ); 68 | } 69 | const countUsers = await User.estimatedDocumentCount(); 70 | if (countUsers === 0) { 71 | const roleSuperAdministrator = await Role.findOne({ name: 'Super Administrator' }); 72 | const roleAdministrator = await Role.findOne({ name: 'Administrator' }); 73 | const roleModerator = await Role.findOne({ name: 'Moderator' }); 74 | const roleUser = await Role.findOne({ name: 'User' }); 75 | await User.create( 76 | { 77 | firstName: 'Thuc', 78 | lastName: 'Nguyen', 79 | userName: 'superadmin', 80 | email: 'admjnwapviip@gmail.com', 81 | password: 'superadmin', 82 | roles: [roleSuperAdministrator, roleAdministrator, roleModerator, roleUser] 83 | }, 84 | { 85 | firstName: 'Vy', 86 | lastName: 'Nguyen', 87 | userName: 'admin', 88 | email: 'admin@example.com', 89 | password: 'admin', 90 | roles: [roleAdministrator] 91 | }, 92 | { 93 | firstName: 'Thuyen', 94 | lastName: 'Nguyen', 95 | userName: 'moderator', 96 | email: 'moderator@example.com', 97 | password: 'moderator', 98 | roles: [roleModerator] 99 | }, 100 | { 101 | firstName: 'Uyen', 102 | lastName: 'Nguyen', 103 | userName: 'user', 104 | email: 'user@example.com', 105 | password: 'user', 106 | roles: [roleUser] 107 | } 108 | ); 109 | } 110 | } catch (err) { 111 | logger.error(err); 112 | } 113 | } 114 | 115 | export default initialData; 116 | -------------------------------------------------------------------------------- /src/config/logger.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import config from './config'; 3 | 4 | const levels = { 5 | error: 0, 6 | warn: 1, 7 | info: 2, 8 | http: 3, 9 | debug: 4 10 | }; 11 | 12 | winston.addColors({ 13 | error: 'red', 14 | warn: 'yellow', 15 | info: 'green', 16 | http: 'magenta', 17 | debug: 'white' 18 | }); 19 | 20 | const logger = winston.createLogger({ 21 | level: config.NODE_ENV === 'development' ? 'debug' : 'warn', 22 | levels, 23 | format: winston.format.combine( 24 | winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 25 | winston.format.printf((info) => `${[info.timestamp]}: ${info.level}: ${info.message}`) 26 | ), 27 | transports: [ 28 | new winston.transports.File({ 29 | level: 'error', 30 | filename: 'logs/error.log', 31 | maxsize: '10000000', 32 | maxFiles: '10' 33 | }), 34 | new winston.transports.File({ 35 | filename: 'logs/combined.log', 36 | maxsize: '10000000', 37 | maxFiles: '10' 38 | }), 39 | new winston.transports.Console({ format: winston.format.combine(winston.format.colorize({ all: true })) }) 40 | ] 41 | }); 42 | 43 | export default logger; 44 | -------------------------------------------------------------------------------- /src/config/mongoose.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import config from './config'; 3 | import logger from './logger'; 4 | 5 | const mongooseConnect = () => { 6 | const reconnectTimeout = 5000; 7 | 8 | const connect = () => { 9 | mongoose.connect(config.DATABASE_URI, { 10 | useNewUrlParser: true, 11 | useUnifiedTopology: true 12 | }); 13 | }; 14 | 15 | mongoose.Promise = global.Promise; 16 | 17 | const db = mongoose.connection; 18 | 19 | db.on('connecting', () => { 20 | logger.info('🚀 Connecting to MongoDB...'); 21 | }); 22 | 23 | db.on('error', (err) => { 24 | logger.error(`MongoDB connection error: ${err}`); 25 | mongoose.disconnect(); 26 | }); 27 | 28 | db.on('connected', () => { 29 | logger.info('🚀 Connected to MongoDB!'); 30 | }); 31 | 32 | db.once('open', () => { 33 | logger.info('🚀 MongoDB connection opened!'); 34 | }); 35 | 36 | db.on('reconnected', () => { 37 | logger.info('🚀 MongoDB reconnected!'); 38 | }); 39 | 40 | db.on('disconnected', () => { 41 | logger.error(`MongoDB disconnected! Reconnecting in ${reconnectTimeout / 1000}s...`); 42 | setTimeout(() => connect(), reconnectTimeout); 43 | }); 44 | 45 | connect(); 46 | }; 47 | 48 | export default mongooseConnect; 49 | -------------------------------------------------------------------------------- /src/config/morgan.js: -------------------------------------------------------------------------------- 1 | import morgan from 'morgan'; 2 | import logger from './logger'; 3 | 4 | const morganHTTP = morgan('combined', { 5 | stream: { write: (message) => logger.http(message.trim()) } 6 | }); 7 | 8 | export default morganHTTP; 9 | -------------------------------------------------------------------------------- /src/config/passport.js: -------------------------------------------------------------------------------- 1 | import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'; 2 | import passport from 'passport'; 3 | import config from './config'; 4 | import User from '~/models/userModel'; 5 | 6 | passport.use( 7 | new JwtStrategy( 8 | { 9 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 10 | secretOrKey: config.JWT_ACCESS_TOKEN_SECRET_PUBLIC, 11 | algorithms: 'RS256' 12 | }, 13 | async (jwtPayload, done) => { 14 | try { 15 | const user = await User.getUserById(jwtPayload.sub); 16 | if (!user) { 17 | return done(null, false); 18 | } 19 | return done(null, user); 20 | } catch (err) { 21 | return done(err, false); 22 | } 23 | } 24 | ) 25 | ); 26 | 27 | export default passport; 28 | -------------------------------------------------------------------------------- /src/controllers/authController.js: -------------------------------------------------------------------------------- 1 | import APIError from '~/utils/apiError'; 2 | import tokenService from '~/services/tokenService'; 3 | import emailService from '~/services/emailService'; 4 | import User from '~/models/userModel'; 5 | import config from '~/config/config'; 6 | import httpStatus from 'http-status'; 7 | import Token from '~/models/tokenModel'; 8 | import Role from '~/models/roleModel'; 9 | 10 | export const signup = async (req, res) => { 11 | const role = await Role.getRoleByName('User'); 12 | req.body.roles = [role.id]; 13 | const user = await User.createUser(req.body); 14 | const tokens = await tokenService.generateAuthTokens(user); 15 | return res.json({ 16 | success: true, 17 | data: { user, tokens } 18 | }); 19 | }; 20 | 21 | export const signin = async (req, res) => { 22 | const user = await User.getUserByUserName(req.body.userName); 23 | if (!user || !(await user.isPasswordMatch(req.body.password))) { 24 | throw new APIError('Incorrect user name or password', httpStatus.BAD_REQUEST); 25 | } 26 | const tokens = await tokenService.generateAuthTokens(user); 27 | return res.json({ 28 | success: true, 29 | data: { user, tokens } 30 | }); 31 | }; 32 | 33 | export const current = async (req, res) => { 34 | const user = await User.getUserById(req.user.id); 35 | if (!user) { 36 | throw new APIError('User not found', httpStatus.NOT_FOUND); 37 | } 38 | return res.json({ 39 | success: true, 40 | data: { 41 | firstName: user.firstName, 42 | lastName: user.lastName, 43 | userName: user.userName, 44 | avatarUrl: user.avatarUrl 45 | } 46 | }); 47 | }; 48 | 49 | export const getMe = async (req, res) => { 50 | const user = await User.getUserByIdWithRoles(req.user.id); 51 | if (!user) { 52 | throw new APIError('User not found', httpStatus.NOT_FOUND); 53 | } 54 | return res.json({ 55 | success: true, 56 | data: user 57 | }); 58 | }; 59 | 60 | export const updateMe = async (req, res) => { 61 | const user = await User.updateUserById(req.user.id, req.body); 62 | return res.json({ 63 | success: true, 64 | data: user 65 | }); 66 | }; 67 | 68 | export const signout = async (req, res) => { 69 | await Token.revokeToken(req.body.refreshToken, config.TOKEN_TYPES.REFRESH); 70 | return res.json({ 71 | success: true, 72 | data: 'Signout success' 73 | }); 74 | }; 75 | 76 | export const refreshTokens = async (req, res) => { 77 | try { 78 | const refreshTokenDoc = await tokenService.verifyToken(req.body.refreshToken, config.TOKEN_TYPES.REFRESH); 79 | const user = await User.getUserById(refreshTokenDoc.user); 80 | if (!user) { 81 | throw new Error(); 82 | } 83 | await refreshTokenDoc.remove(); 84 | const tokens = await tokenService.generateAuthTokens(user); 85 | return res.json({ 86 | success: true, 87 | data: { 88 | tokens 89 | } 90 | }); 91 | } catch (err) { 92 | throw new APIError(err.message, httpStatus.UNAUTHORIZED); 93 | } 94 | }; 95 | 96 | export const sendVerificationEmail = async (req, res) => { 97 | const user = await User.getUserByEmail(req.user.email); 98 | if (user.confirmed) { 99 | throw new APIError('Email verified', httpStatus.BAD_REQUEST); 100 | } 101 | const verifyEmailToken = await tokenService.generateVerifyEmailToken(req.user); 102 | await emailService.sendVerificationEmail(req.user.email, verifyEmailToken); 103 | return res.json({ 104 | success: true, 105 | data: 'Send verification email success' 106 | }); 107 | }; 108 | 109 | export const verifyEmail = async (req, res) => { 110 | try { 111 | const verifyEmailTokenDoc = await tokenService.verifyToken(req.query.token, config.TOKEN_TYPES.VERIFY_EMAIL); 112 | const user = await User.getUserById(verifyEmailTokenDoc.user); 113 | if (!user) { 114 | throw new Error(); 115 | } 116 | await Token.deleteMany({ user: user.id, type: config.TOKEN_TYPES.VERIFY_EMAIL }); 117 | await User.updateUserById(user.id, { confirmed: true }); 118 | return res.json({ 119 | success: true, 120 | data: 'Verify email success' 121 | }); 122 | } catch (err) { 123 | throw new APIError('Email verification failed', httpStatus.UNAUTHORIZED); 124 | } 125 | }; 126 | 127 | export const forgotPassword = async (req, res) => { 128 | const resetPasswordToken = await tokenService.generateResetPasswordToken(req.body.email); 129 | await emailService.sendResetPasswordEmail(req.body.email, resetPasswordToken); 130 | return res.json({ 131 | success: true, 132 | data: 'Send forgot password email success' 133 | }); 134 | }; 135 | 136 | export const resetPassword = async (req, res) => { 137 | try { 138 | const resetPasswordTokenDoc = await tokenService.verifyToken(req.query.token, config.TOKEN_TYPES.RESET_PASSWORD); 139 | const user = await User.getUserById(resetPasswordTokenDoc.user); 140 | if (!user) { 141 | throw new Error(); 142 | } 143 | await Token.deleteMany({ user: user.id, type: config.TOKEN_TYPES.RESET_PASSWORD }); 144 | await User.updateUserById(user.id, { password: req.body.password }); 145 | return res.json({ 146 | success: true, 147 | data: 'Reset password success' 148 | }); 149 | } catch (err) { 150 | throw new APIError('Password reset failed', httpStatus.UNAUTHORIZED); 151 | } 152 | }; 153 | 154 | export default { 155 | signup, 156 | signin, 157 | current, 158 | getMe, 159 | updateMe, 160 | signout, 161 | refreshTokens, 162 | sendVerificationEmail, 163 | verifyEmail, 164 | forgotPassword, 165 | resetPassword 166 | }; 167 | -------------------------------------------------------------------------------- /src/controllers/imageController.js: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import APIError from '~/utils/apiError'; 3 | import ResizeImage from '~/utils/resizeImage'; 4 | 5 | export const uploadImage = async (req, res) => { 6 | if (!req.file) { 7 | throw new APIError('Please provide an image', httpStatus.BAD_REQUEST); 8 | } 9 | const fileName = await ResizeImage(req.file.destination, req.file.filename); 10 | return res.json({ image: fileName }); 11 | }; 12 | 13 | export default { uploadImage }; 14 | -------------------------------------------------------------------------------- /src/controllers/roleController.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import APIError from '~/utils/apiError'; 3 | import User from '~/models/userModel'; 4 | import Role from '~/models/roleModel'; 5 | import httpStatus from 'http-status'; 6 | 7 | export const createRole = async (req, res) => { 8 | const role = await Role.createRole(req.body); 9 | return res.status(200).json({ 10 | success: true, 11 | data: role 12 | }); 13 | }; 14 | 15 | export const getRole = async (req, res) => { 16 | const role = await Role.getRoleById(req.params.roleId); 17 | if (!role) { 18 | throw new APIError('Role not found', httpStatus.NOT_FOUND); 19 | } 20 | return res.json({ 21 | success: true, 22 | data: role 23 | }); 24 | }; 25 | 26 | export const updateRole = async (req, res) => { 27 | const role = await Role.updateRoleById(req.params.roleId, req.body); 28 | return res.json({ 29 | success: true, 30 | data: role 31 | }); 32 | }; 33 | 34 | export const getRoles = async (req, res) => { 35 | const filters = _.pick(req.query, ['q']); 36 | const options = _.pick(req.query, ['limit', 'page', 'sortBy', 'sortDirection']); 37 | const roles = await Role.paginate( 38 | options, 39 | 'permissions', 40 | filters.q && { 41 | $or: [ 42 | { 43 | name: { 44 | $regex: filters.q, 45 | $options: 'i' 46 | } 47 | }, 48 | { 49 | description: { 50 | $regex: filters.q, 51 | $options: 'i' 52 | } 53 | } 54 | ] 55 | } 56 | ); 57 | return res.json({ 58 | success: true, 59 | data: roles.results, 60 | pagination: { 61 | total: roles.totalResults 62 | } 63 | }); 64 | }; 65 | 66 | export const deleteRole = async (req, res) => { 67 | if (await User.isRoleIdAlreadyExists(req.params.roleId)) { 68 | throw new APIError('A role cannot be deleted if associated with users', httpStatus.BAD_REQUEST); 69 | } 70 | await Role.deleteRoleById(req.params.roleId); 71 | return res.json({ 72 | success: true, 73 | data: {} 74 | }); 75 | }; 76 | 77 | export default { createRole, getRole, updateRole, getRoles, deleteRole }; 78 | -------------------------------------------------------------------------------- /src/controllers/userController.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import APIError from '~/utils/apiError'; 3 | import User from '~/models/userModel'; 4 | import Role from '~/models/roleModel'; 5 | import httpStatus from 'http-status'; 6 | 7 | export const createUser = async (req, res) => { 8 | const user = await User.createUser(req.body); 9 | return res.status(200).json({ 10 | success: true, 11 | data: user 12 | }); 13 | }; 14 | 15 | export const getUsers = async (req, res) => { 16 | const filters = _.pick(req.query, ['q']); 17 | const options = _.pick(req.query, ['limit', 'page', 'sortBy', 'sortDirection']); 18 | const users = await User.paginate( 19 | options, 20 | 'roles.permissions', 21 | filters.q && { 22 | $or: [ 23 | { 24 | firstName: { 25 | $regex: filters.q, 26 | $options: 'i' 27 | } 28 | }, 29 | { 30 | lastName: { 31 | $regex: filters.q, 32 | $options: 'i' 33 | } 34 | }, 35 | { 36 | userName: { 37 | $regex: filters.q, 38 | $options: 'i' 39 | } 40 | } 41 | ] 42 | } 43 | ); 44 | return res.json({ 45 | success: true, 46 | data: users.results, 47 | pagination: { 48 | total: users.totalResults 49 | } 50 | }); 51 | }; 52 | 53 | export const getUser = async (req, res) => { 54 | const user = await User.getUserByIdWithRoles(req.params.userId); 55 | if (!user) { 56 | throw new APIError('User not found', httpStatus.NOT_FOUND); 57 | } 58 | return res.json({ 59 | success: true, 60 | data: user 61 | }); 62 | }; 63 | 64 | export const updateUser = async (req, res) => { 65 | const role = await Role.getRoleByName('Super Administrator'); 66 | if (req.body.roles && !(await User.isRoleIdAlreadyExists(role.id, req.params.userId)) && !req.body.roles.includes(role.id)) { 67 | throw new APIError('Requires at least 1 user as Super Administrator', httpStatus.BAD_REQUEST); 68 | } 69 | const user = await User.updateUserById(req.params.userId, req.body); 70 | return res.json({ 71 | success: true, 72 | data: user 73 | }); 74 | }; 75 | 76 | export const deleteUser = async (req, res) => { 77 | const role = await Role.getRoleByName('Super Administrator'); 78 | if (!(await User.isRoleIdAlreadyExists(role.id, req.params.userId))) { 79 | throw new APIError('Requires at least 1 user as Super Administrator', httpStatus.BAD_REQUEST); 80 | } 81 | await User.deleteUserById(req.params.userId); 82 | return res.json({ 83 | success: true, 84 | data: 'Delete user success' 85 | }); 86 | }; 87 | 88 | export default { createUser, getUsers, getUser, updateUser, deleteUser }; 89 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import config from '~/config/config'; 3 | import app from './app'; 4 | import initialData from './config/initialData'; 5 | import logger from './config/logger'; 6 | 7 | let server; 8 | 9 | mongoose.Promise = global.Promise; 10 | 11 | const db = mongoose.connection; 12 | 13 | db.on('connecting', () => { 14 | logger.info('🚀 Connecting to MongoDB...'); 15 | }); 16 | 17 | db.on('error', (err) => { 18 | logger.error(`MongoDB connection error: ${err}`); 19 | mongoose.disconnect(); 20 | }); 21 | 22 | db.on('connected', () => { 23 | logger.info('🚀 Connected to MongoDB!'); 24 | }); 25 | 26 | db.once('open', () => { 27 | logger.info('🚀 MongoDB connection opened!'); 28 | }); 29 | 30 | db.on('reconnected', () => { 31 | logger.info('🚀 MongoDB reconnected!'); 32 | }); 33 | 34 | const connect = async () => { 35 | try { 36 | await mongoose.connect(config.DATABASE_URI, config.DATABASE_OPTIONS); 37 | logger.info('🚀 Connected to MongoDB end!'); 38 | await initialData(); 39 | logger.info('🚀 Initial MongoDB!'); 40 | server = app.listen(config.PORT, config.HOST, () => { 41 | logger.info(`🚀 Host: http://${config.HOST}:${config.PORT}`); 42 | logger.info('██████╗░░░██╗██╗███████╗'); 43 | logger.info('██╔══██╗░██╔╝██║╚════██║'); 44 | logger.info('██║░░██║██╔╝░██║░░███╔═╝'); 45 | logger.info('██║░░██║███████║██╔══╝░░'); 46 | logger.info('██████╔╝╚════██║███████╗'); 47 | logger.info('╚═════╝░░░░░░╚═╝╚══════╝'); 48 | }); 49 | } catch (err) { 50 | logger.error(`MongoDB connection error: ${err}`); 51 | } 52 | }; 53 | 54 | connect(); 55 | 56 | const exitHandler = () => { 57 | if (server) { 58 | server.close(() => { 59 | logger.warn('Server closed'); 60 | process.exit(1); 61 | }); 62 | } else { 63 | process.exit(1); 64 | } 65 | }; 66 | 67 | const unexpectedErrorHandler = (err) => { 68 | logger.error(err); 69 | exitHandler(); 70 | }; 71 | 72 | process.on('uncaughtException', unexpectedErrorHandler); 73 | process.on('unhandledRejection', unexpectedErrorHandler); 74 | 75 | process.on('SIGTERM', () => { 76 | logger.info('SIGTERM received'); 77 | if (server) { 78 | server.close(); 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /src/middlewares/authenticate.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import httpStatus from 'http-status'; 3 | import APIError from '~/utils/apiError'; 4 | import Role from '~/models/roleModel'; 5 | 6 | const verifyCallback = (req, resolve, reject, requiredRights) => async (err, user, info) => { 7 | if (err || info || !user) { 8 | return reject(new APIError(httpStatus[httpStatus.UNAUTHORIZED], httpStatus.UNAUTHORIZED)); 9 | } 10 | req.user = user; 11 | if (requiredRights.length) { 12 | const userRights = []; 13 | const roles = await Role.find({ _id: { $in: user.roles } }).populate('permissions'); 14 | roles.forEach((i) => { 15 | i.permissions.forEach((j) => { 16 | userRights.push(`${j.controller}:${j.action}`); 17 | }); 18 | }); 19 | const hasRequiredRights = requiredRights.every((r) => userRights.includes(r)); 20 | //console.log('requiredRights: ', requiredRights); 21 | //console.log('userRights: ', userRights); 22 | //console.log('boolean: ', hasRequiredRights); 23 | if (!hasRequiredRights) { 24 | return reject(new APIError('Resource access denied', httpStatus.FORBIDDEN)); 25 | } 26 | } 27 | return resolve(); 28 | }; 29 | 30 | const authenticate = 31 | (...requiredRights) => 32 | async (req, res, next) => { 33 | return new Promise((resolve, reject) => { 34 | passport.authenticate('jwt', { session: false }, verifyCallback(req, resolve, reject, requiredRights))(req, res, next); 35 | }) 36 | .then(() => next()) 37 | .catch((err) => next(err)); 38 | }; 39 | 40 | export default authenticate; 41 | -------------------------------------------------------------------------------- /src/middlewares/error.js: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import Joi from 'joi'; 3 | import config from '~/config/config'; 4 | import logger from '~/config/logger'; 5 | import APIError from '~/utils/apiError'; 6 | 7 | export const converter = (err, req, res, next) => { 8 | if (err instanceof Joi.ValidationError) { 9 | const errorMessage = err.details.map((d) => { 10 | return { 11 | message: d.message, 12 | location: d.path[1], 13 | locationType: d.path[0] 14 | }; 15 | }); 16 | const apiError = new APIError(errorMessage, httpStatus.BAD_REQUEST); 17 | apiError.stack = err.stack; 18 | return next(apiError); 19 | } else if (!(err instanceof APIError)) { 20 | const status = err.status || httpStatus.INTERNAL_SERVER_ERROR; 21 | const message = err.message || httpStatus[status]; 22 | const apiError = new APIError(message, status, false); 23 | apiError.stack = err.stack; 24 | apiError.message = [{ message: err.message }]; 25 | return next(apiError); 26 | } 27 | err.message = [{ message: err.message }]; 28 | return next(err); 29 | }; 30 | 31 | export const notFound = (req, res, next) => { 32 | return next(new APIError(httpStatus[httpStatus.NOT_FOUND], httpStatus.NOT_FOUND)); 33 | }; 34 | 35 | export const handler = (err, req, res, next) => { 36 | let { status, message } = err; 37 | if (config.NODE_ENV === 'production' && !err.isOperational) { 38 | status = httpStatus.INTERNAL_SERVER_ERROR; 39 | message = httpStatus[httpStatus.INTERNAL_SERVER_ERROR]; 40 | } 41 | logger.error(err.stack); 42 | return res.status(status).json({ 43 | status: status, 44 | errors: message, 45 | ...(config.NODE_ENV === 'development' && { stack: err.stack }) 46 | }); 47 | }; 48 | 49 | export default { converter, notFound, handler }; 50 | -------------------------------------------------------------------------------- /src/middlewares/rateLimiter.js: -------------------------------------------------------------------------------- 1 | import rateLimit from 'express-rate-limit'; 2 | import httpStatus from 'http-status'; 3 | import APIError from '~/utils/apiError'; 4 | 5 | const rateLimiter = rateLimit({ 6 | windowMs: 15 * 60 * 1000, // 15 minutes 7 | max: 100, 8 | handler: (req, res, next) => { 9 | next(new APIError('Too many requests, please try again later.', httpStatus.TOO_MANY_REQUESTS)); 10 | } 11 | }); 12 | 13 | export default rateLimiter; 14 | -------------------------------------------------------------------------------- /src/middlewares/uploadImage.js: -------------------------------------------------------------------------------- 1 | import multer from 'multer'; 2 | import path from 'path'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import APIError from '~/utils/apiError'; 5 | import fs from 'fs'; 6 | import httpStatus from 'http-status'; 7 | 8 | const storage = multer.diskStorage({ 9 | destination: (req, file, callback) => { 10 | const dir = 'public/images'; 11 | if (!fs.existsSync(dir)) { 12 | fs.mkdirSync(dir, { recursive: true }); 13 | } 14 | callback(null, dir); 15 | }, 16 | filename: (req, file, callback) => { 17 | callback(null, uuidv4() + path.extname(file.originalname)); 18 | } 19 | }); 20 | 21 | const upload = multer({ 22 | storage: storage, 23 | limits: { 24 | fileSize: 6 * 1024 * 1024 25 | }, 26 | fileFilter: (req, file, callback) => { 27 | var ext = path.extname(file.originalname); 28 | if (ext !== '.png' && ext !== '.jpg' && ext !== '.gif' && ext !== '.jpeg') { 29 | return callback(new APIError('File image unsupported', httpStatus.BAD_REQUEST)); 30 | } 31 | callback(null, true); 32 | } 33 | }).single('image'); 34 | 35 | const uploadImage = (req, res, next) => { 36 | upload(req, res, (err) => { 37 | if (err instanceof multer.MulterError) { 38 | return next(new APIError(err.message, httpStatus.BAD_REQUEST)); 39 | } else if (err) { 40 | return next(err); 41 | } 42 | return next(); 43 | }); 44 | }; 45 | 46 | export default uploadImage; 47 | -------------------------------------------------------------------------------- /src/middlewares/validate.js: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import _ from 'lodash'; 3 | 4 | const validate = (schema) => (req, res, next) => { 5 | const validSchema = _.pick(schema, ['params', 'query', 'body']); 6 | const object = _.pick(req, Object.keys(validSchema)); 7 | const { error, value } = Joi.compile(validSchema) 8 | .prefs({ errors: { label: 'path', wrap: { label: false } }, abortEarly: false }) 9 | .validate(object); 10 | if (error) { 11 | return next(error); 12 | } 13 | Object.assign(req, value); 14 | return next(); 15 | }; 16 | 17 | export default validate; 18 | -------------------------------------------------------------------------------- /src/models/permissionModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import toJSON from './plugins/toJSONPlugin'; 3 | 4 | const permissionSchema = mongoose.Schema( 5 | { 6 | controller: { 7 | type: String, 8 | required: true 9 | }, 10 | action: { 11 | type: String, 12 | required: true 13 | }, 14 | enabled: { 15 | type: Boolean, 16 | default: true 17 | } 18 | }, 19 | { 20 | timestamps: true 21 | } 22 | ); 23 | 24 | permissionSchema.index({ controller: 1, action: 1 }, { unique: true }); 25 | 26 | permissionSchema.plugin(toJSON); 27 | 28 | const Permission = mongoose.model('permissions', permissionSchema); 29 | 30 | export default Permission; 31 | -------------------------------------------------------------------------------- /src/models/plugins/paginatePlugin.js: -------------------------------------------------------------------------------- 1 | const paginate = (schema) => { 2 | schema.statics.paginate = async function paginateFunc(options, populate, query) { 3 | const sortBy = options.sortBy ? options.sortBy : 'createdAt'; 4 | const sortDirection = options.sortDirection && options.sortDirection === 'asc' ? 'asc' : 'desc'; 5 | const page = options.page && parseInt(options.page, 10) > 0 ? parseInt(options.page, 10) : 1; 6 | const limit = options.limit && parseInt(options.limit, 10) > 0 ? parseInt(options.limit, 10) : 10; 7 | const skip = (page - 1) * limit; 8 | 9 | const countPromise = this.countDocuments(query).exec(); 10 | let docsPromise = this.find(query) 11 | .sort({ [sortBy]: sortDirection }) 12 | .skip(skip) 13 | .limit(limit); 14 | 15 | if (populate) { 16 | populate.split(' ').forEach((populate) => { 17 | docsPromise = docsPromise.populate( 18 | populate 19 | .split('.') 20 | .reverse() 21 | .reduce((a, b) => ({ path: b, populate: a })) 22 | ); 23 | }); 24 | } 25 | 26 | docsPromise = docsPromise.exec(); 27 | 28 | const [totalResults, results] = await Promise.all([countPromise, docsPromise]); 29 | 30 | return { 31 | results, 32 | totalResults 33 | }; 34 | }; 35 | }; 36 | 37 | export default paginate; 38 | -------------------------------------------------------------------------------- /src/models/plugins/toJSONPlugin.js: -------------------------------------------------------------------------------- 1 | function normalizeId(ret) { 2 | if (ret._id && typeof ret._id === 'object' && ret._id.toString) { 3 | if (typeof ret.id === 'undefined') { 4 | ret.id = ret._id.toString(); 5 | } 6 | } 7 | if (typeof ret._id !== 'undefined') { 8 | delete ret._id; 9 | } 10 | } 11 | 12 | function removePrivatePaths(ret, schema) { 13 | for (const path in schema.paths) { 14 | if (schema.paths[path].options && schema.paths[path].options.private) { 15 | if (typeof ret[path] !== 'undefined') { 16 | delete ret[path]; 17 | } 18 | } 19 | } 20 | } 21 | 22 | function removeVersion(ret) { 23 | if (typeof ret.__v !== 'undefined') { 24 | delete ret.__v; 25 | } 26 | } 27 | 28 | function toJSON(schema) { 29 | // NOTE: this plugin is actually called *after* any schema's 30 | // custom toJSON has been defined, so we need to ensure not to 31 | // overwrite it. Hence, we remember it here and call it later 32 | let transform; 33 | if (schema.options.toJSON && schema.options.toJSON.transform) { 34 | transform = schema.options.toJSON.transform; 35 | } 36 | 37 | // Extend toJSON options 38 | schema.options.toJSON = Object.assign(schema.options.toJSON || {}, { 39 | transform(doc, ret, options) { 40 | // Remove private paths 41 | if (schema.options.removePrivatePaths !== false) { 42 | removePrivatePaths(ret, schema); 43 | } 44 | 45 | // Remove version 46 | if (schema.options.removeVersion !== false) { 47 | removeVersion(ret); 48 | } 49 | 50 | // Normalize ID 51 | if (schema.options.normalizeId !== false) { 52 | normalizeId(ret); 53 | } 54 | 55 | // Call custom transform if present 56 | if (transform) { 57 | return transform(doc, ret, options); 58 | } 59 | 60 | return ret; 61 | } 62 | }); 63 | } 64 | 65 | export default toJSON; 66 | -------------------------------------------------------------------------------- /src/models/roleModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import APIError from '~/utils/apiError'; 3 | import paginate from './plugins/paginatePlugin'; 4 | import toJSON from './plugins/toJSONPlugin'; 5 | import Permission from './permissionModel'; 6 | import httpStatus from 'http-status'; 7 | 8 | const roleSchema = mongoose.Schema( 9 | { 10 | name: { 11 | type: String, 12 | required: true, 13 | unique: true 14 | }, 15 | description: { 16 | type: String, 17 | default: '' 18 | }, 19 | permissions: [ 20 | { 21 | type: mongoose.SchemaTypes.ObjectId, 22 | ref: 'permissions' 23 | } 24 | ] 25 | }, 26 | { 27 | timestamps: true 28 | } 29 | ); 30 | 31 | roleSchema.plugin(toJSON); 32 | roleSchema.plugin(paginate); 33 | 34 | class RoleClass { 35 | static async isNameAlreadyExists(name, excludeUserId) { 36 | return !!(await this.findOne({ name, _id: { $ne: excludeUserId } })); 37 | } 38 | 39 | static async getRoleByName(name) { 40 | return await this.findOne({ name: name }); 41 | } 42 | 43 | static async getRoleById(id) { 44 | return await this.findById(id); 45 | } 46 | 47 | static async createRole(body) { 48 | if (await this.isNameAlreadyExists(body.name)) { 49 | throw new APIError('Name already exists', httpStatus.BAD_REQUEST); 50 | } 51 | if (body.permissions) { 52 | await Promise.all( 53 | body.permissions.map(async (pid) => { 54 | if (!(await Permission.findById(pid))) { 55 | throw new APIError('Permissions not exist', httpStatus.BAD_REQUEST); 56 | } 57 | }) 58 | ); 59 | } 60 | return await this.create(body); 61 | } 62 | 63 | static async updateRoleById(roleId, body) { 64 | const role = await this.getRoleById(roleId); 65 | if (!role) { 66 | throw new APIError('Role not found', httpStatus.NOT_FOUND); 67 | } 68 | if (await this.isNameAlreadyExists(body.name, roleId)) { 69 | throw new APIError('Name already exists', httpStatus.BAD_REQUEST); 70 | } 71 | if (body.permissions) { 72 | await Promise.all( 73 | body.permissions.map(async (pid) => { 74 | if (!(await Permission.findById(pid))) { 75 | throw new APIError('Permissions not exist', httpStatus.BAD_REQUEST); 76 | } 77 | }) 78 | ); 79 | } 80 | Object.assign(role, body); 81 | return await role.save(); 82 | } 83 | 84 | static async deleteRoleById(roleId) { 85 | const role = await this.getRoleById(roleId); 86 | if (!role) { 87 | throw new APIError('Role not found', httpStatus.NOT_FOUND); 88 | } 89 | return await role.remove(); 90 | } 91 | } 92 | 93 | roleSchema.loadClass(RoleClass); 94 | 95 | const Role = mongoose.model('roles', roleSchema); 96 | 97 | export default Role; 98 | -------------------------------------------------------------------------------- /src/models/tokenModel.js: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import mongoose from 'mongoose'; 3 | import config from '~/config/config'; 4 | import APIError from '~/utils/apiError'; 5 | import toJSON from './plugins/toJSONPlugin'; 6 | 7 | const tokenSchema = mongoose.Schema( 8 | { 9 | user: { 10 | type: mongoose.SchemaTypes.ObjectId, 11 | ref: 'users', 12 | required: true 13 | }, 14 | token: { 15 | type: String, 16 | required: true, 17 | index: true 18 | }, 19 | type: { 20 | type: String, 21 | enum: [config.TOKEN_TYPES.REFRESH, config.TOKEN_TYPES.RESET_PASSWORD, config.TOKEN_TYPES.VERIFY_EMAIL], 22 | required: true 23 | }, 24 | blacklisted: { 25 | type: Boolean, 26 | default: false 27 | }, 28 | expiresAt: { 29 | type: Date, 30 | required: true 31 | } 32 | }, 33 | { 34 | timestamps: true 35 | } 36 | ); 37 | 38 | tokenSchema.plugin(toJSON); 39 | 40 | class TokenClass { 41 | static async saveToken(token, userId, expires, type, blacklisted = false) { 42 | const tokenDoc = await this.create({ 43 | user: userId, 44 | token, 45 | type, 46 | expiresAt: expires, 47 | blacklisted 48 | }); 49 | return tokenDoc; 50 | } 51 | 52 | static async revokeToken(token, type) { 53 | const tokenDoc = await this.findOne({ token: token, type: type, blacklisted: false }); 54 | if (!tokenDoc) { 55 | throw new APIError('Token not found', httpStatus.BAD_REQUEST); 56 | } 57 | await tokenDoc.remove(); 58 | } 59 | } 60 | 61 | tokenSchema.loadClass(TokenClass); 62 | 63 | const Token = mongoose.model('tokens', tokenSchema); 64 | 65 | export default Token; 66 | -------------------------------------------------------------------------------- /src/models/userModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import bcrypt from 'bcryptjs'; 3 | import paginate from './plugins/paginatePlugin'; 4 | import toJSON from './plugins/toJSONPlugin'; 5 | import APIError from '~/utils/apiError'; 6 | import Role from './roleModel'; 7 | import config from '~/config/config'; 8 | import httpStatus from 'http-status'; 9 | 10 | const userSchema = mongoose.Schema( 11 | { 12 | firstName: { 13 | type: String, 14 | required: true 15 | }, 16 | lastName: { 17 | type: String, 18 | required: true 19 | }, 20 | userName: { 21 | type: String, 22 | required: true, 23 | unique: true 24 | }, 25 | email: { 26 | type: String, 27 | required: true, 28 | unique: true 29 | }, 30 | password: { 31 | type: String, 32 | required: true, 33 | private: true 34 | }, 35 | avatar: { 36 | type: String, 37 | default: 'avatar.png' 38 | }, 39 | confirmed: { 40 | type: Boolean, 41 | default: false 42 | }, 43 | roles: [ 44 | { 45 | type: mongoose.SchemaTypes.ObjectId, 46 | ref: 'roles' 47 | } 48 | ] 49 | }, 50 | { 51 | timestamps: true, 52 | toJSON: { virtuals: true } 53 | } 54 | ); 55 | 56 | userSchema.plugin(toJSON); 57 | userSchema.plugin(paginate); 58 | 59 | userSchema.virtual('avatarUrl').get(function () { 60 | return config.IMAGE_URL + '/' + this.avatar; 61 | }); 62 | 63 | class UserClass { 64 | static async isUserNameAlreadyExists(userName, excludeUserId) { 65 | return !!(await this.findOne({ userName, _id: { $ne: excludeUserId } })); 66 | } 67 | 68 | static async isEmailAlreadyExists(email, excludeUserId) { 69 | return !!(await this.findOne({ email, _id: { $ne: excludeUserId } })); 70 | } 71 | 72 | static async isRoleIdAlreadyExists(roleId, excludeUserId) { 73 | return !!(await this.findOne({ roles: roleId, _id: { $ne: excludeUserId } })); 74 | } 75 | 76 | static async getUserById(id) { 77 | return await this.findById(id); 78 | } 79 | 80 | static async getUserByIdWithRoles(id) { 81 | return await this.findById(id).populate({ path: 'roles', select: 'name description createdAt updatedAt' }); 82 | } 83 | 84 | static async getUserByUserName(userName) { 85 | return await this.findOne({ userName }); 86 | } 87 | 88 | static async getUserByEmail(email) { 89 | return await this.findOne({ email }); 90 | } 91 | 92 | static async createUser(body) { 93 | if (await this.isUserNameAlreadyExists(body.userName)) { 94 | throw new APIError('User name already exists', httpStatus.BAD_REQUEST); 95 | } 96 | if (await this.isEmailAlreadyExists(body.email)) { 97 | throw new APIError('Email already exists', httpStatus.BAD_REQUEST); 98 | } 99 | if (body.roles) { 100 | await Promise.all( 101 | body.roles.map(async (rid) => { 102 | if (!(await Role.findById(rid))) { 103 | throw new APIError('Roles not exist', httpStatus.BAD_REQUEST); 104 | } 105 | }) 106 | ); 107 | } 108 | return await this.create(body); 109 | } 110 | 111 | static async updateUserById(userId, body) { 112 | const user = await this.getUserById(userId); 113 | if (!user) { 114 | throw new APIError('User not found', httpStatus.NOT_FOUND); 115 | } 116 | if (await this.isUserNameAlreadyExists(body.userName, userId)) { 117 | throw new APIError('User name already exists', httpStatus.BAD_REQUEST); 118 | } 119 | if (await this.isEmailAlreadyExists(body.email, userId)) { 120 | throw new APIError('Email already exists', httpStatus.BAD_REQUEST); 121 | } 122 | if (body.roles) { 123 | await Promise.all( 124 | body.roles.map(async (rid) => { 125 | if (!(await Role.findById(rid))) { 126 | throw new APIError('Roles not exist', httpStatus.BAD_REQUEST); 127 | } 128 | }) 129 | ); 130 | } 131 | Object.assign(user, body); 132 | return await user.save(); 133 | } 134 | 135 | static async deleteUserById(userId) { 136 | const user = await this.getUserById(userId); 137 | if (!user) { 138 | throw new APIError('User not found', httpStatus.NOT_FOUND); 139 | } 140 | return await user.remove(); 141 | } 142 | 143 | async isPasswordMatch(password) { 144 | return bcrypt.compareSync(password, this.password); 145 | } 146 | } 147 | 148 | userSchema.loadClass(UserClass); 149 | 150 | userSchema.pre('save', async function (next) { 151 | if (this.isModified('password')) { 152 | const passwordGenSalt = bcrypt.genSaltSync(10); 153 | this.password = bcrypt.hashSync(this.password, passwordGenSalt); 154 | } 155 | next(); 156 | }); 157 | 158 | const User = mongoose.model('users', userSchema); 159 | 160 | export default User; 161 | -------------------------------------------------------------------------------- /src/routes/v1/authRoute.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import catchAsync from '~/utils/catchAsync'; 3 | import validate from '~/middlewares/validate'; 4 | import authenticate from '~/middlewares/authenticate'; 5 | import authValidation from '~/validations/authValidation'; 6 | import authController from '~/controllers/authController'; 7 | 8 | const router = Router(); 9 | 10 | router.post('/signup', validate(authValidation.signup), catchAsync(authController.signup)); 11 | router.post('/signin', validate(authValidation.signin), catchAsync(authController.signin)); 12 | router.get('/current', authenticate(), catchAsync(authController.current)); 13 | router.get('/me', authenticate(), catchAsync(authController.getMe)); 14 | router.put('/me', authenticate(), validate(authValidation.updateMe), catchAsync(authController.updateMe)); 15 | router.post('/signout', validate(authValidation.signout), catchAsync(authController.signout)); 16 | router.post('/refresh-tokens', validate(authValidation.refreshTokens), catchAsync(authController.refreshTokens)); 17 | router.post('/send-verification-email', authenticate(), catchAsync(authController.sendVerificationEmail)); 18 | router.post('/verify-email', validate(authValidation.verifyEmail), catchAsync(authController.verifyEmail)); 19 | router.post('/forgot-password', validate(authValidation.forgotPassword), catchAsync(authController.forgotPassword)); 20 | router.post('/reset-password', validate(authValidation.resetPassword), catchAsync(authController.resetPassword)); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /src/routes/v1/imageRoute.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import catchAsync from '~/utils/catchAsync'; 3 | import imageController from '~/controllers/imageController'; 4 | import uploadImage from '~/middlewares/uploadImage'; 5 | import authenticate from '~/middlewares/authenticate'; 6 | 7 | const router = Router(); 8 | 9 | router.post('/upload', authenticate(), uploadImage, catchAsync(imageController.uploadImage)); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /src/routes/v1/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import authRoute from './authRoute'; 3 | import userRoute from './userRoute'; 4 | import roleRoute from './roleRoute'; 5 | import imageRoute from './imageRoute'; 6 | 7 | const router = Router(); 8 | 9 | router.use('/auth', authRoute); 10 | router.use('/users', userRoute); 11 | router.use('/roles', roleRoute); 12 | router.use('/images', imageRoute); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /src/routes/v1/roleRoute.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import catchAsync from '~/utils/catchAsync'; 3 | import validate from '~/middlewares/validate'; 4 | import authenticate from '~/middlewares/authenticate'; 5 | import roleValidation from '~/validations/roleValidation'; 6 | import roleController from '~/controllers/roleController'; 7 | 8 | const router = Router(); 9 | 10 | router.get('/', authenticate('role:read'), validate(roleValidation.getRoles), catchAsync(roleController.getRoles)); 11 | router.post('/', authenticate('role:create'), validate(roleValidation.createRole), catchAsync(roleController.createRole)); 12 | router.get('/:roleId', authenticate('role:read'), validate(roleValidation.getRole), catchAsync(roleController.getRole)); 13 | router.put('/:roleId', authenticate('role:update'), validate(roleValidation.updateRole), catchAsync(roleController.updateRole)); 14 | router.delete( 15 | '/:roleId', 16 | authenticate('role:delete'), 17 | validate(roleValidation.deleteRole), 18 | catchAsync(roleController.deleteRole) 19 | ); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /src/routes/v1/userRoute.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import catchAsync from '~/utils/catchAsync'; 3 | import validate from '~/middlewares/validate'; 4 | import authenticate from '~/middlewares/authenticate'; 5 | import userValidation from '~/validations/userValidation'; 6 | import userController from '~/controllers/userController'; 7 | 8 | const router = Router(); 9 | 10 | router.get('/', authenticate('user:read'), validate(userValidation.getUsers), catchAsync(userController.getUsers)); 11 | router.post('/', authenticate('user:create'), validate(userValidation.createUser), catchAsync(userController.createUser)); 12 | router.get('/:userId', authenticate('user:read'), validate(userValidation.getUser), catchAsync(userController.getUser)); 13 | router.put('/:userId', authenticate('user:update'), validate(userValidation.updateUser), catchAsync(userController.updateUser)); 14 | router.delete( 15 | '/:userId', 16 | authenticate('user:delete'), 17 | validate(userValidation.deleteUser), 18 | catchAsync(userController.deleteUser) 19 | ); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /src/services/emailService/index.js: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import logger from '~/config/logger'; 3 | import template from './template'; 4 | import config from '~/config/config'; 5 | 6 | export const transport = nodemailer.createTransport({ 7 | host: config.SMTP_HOST, 8 | port: config.SMTP_PORT, 9 | secure: true, 10 | auth: { 11 | user: config.SMTP_USERNAME, 12 | pass: config.SMTP_PASSWORD 13 | } 14 | }); 15 | 16 | if (config.NODE_ENV !== 'test') { 17 | transport 18 | .verify() 19 | .then(() => logger.info('Connected to email server')) 20 | .catch(() => logger.warn('Unable to connect to email server')); 21 | } 22 | 23 | export const sendEmail = async (to, subject, html) => { 24 | const msg = { from: `${config.APP_NAME} <${config.EMAIL_FROM}>`, to, subject, html }; 25 | await transport.sendMail(msg); 26 | }; 27 | 28 | export const sendResetPasswordEmail = async (to, token) => { 29 | const subject = 'Reset password'; 30 | const resetPasswordUrl = `${config.FRONTEND_URL}/reset-password?token=${token}`; 31 | const html = template.resetPassword(resetPasswordUrl, config.APP_NAME); 32 | await sendEmail(to, subject, html); 33 | }; 34 | 35 | export const sendVerificationEmail = async (to, token) => { 36 | const subject = 'Email Verification'; 37 | const verificationEmailUrl = `${config.FRONTEND_URL}/verify-email?token=${token}`; 38 | const html = template.verifyEmail(verificationEmailUrl, config.APP_NAME); 39 | await sendEmail(to, subject, html); 40 | }; 41 | 42 | export default { sendEmail, sendResetPasswordEmail, sendVerificationEmail }; 43 | -------------------------------------------------------------------------------- /src/services/emailService/template.js: -------------------------------------------------------------------------------- 1 | export const verifyEmail = (url, appName) => { 2 | return ` 3 | 5 | 6 | 7 | 8 | 9 | 10 | Verify your email address 11 | 197 | 198 | 199 | 200 | 201 | 202 | 264 | 265 |
203 | 204 | 205 | 206 | 209 | 210 | 211 | 212 | 248 | 249 | 250 | 261 | 262 | 263 |
266 | 267 | 268 | 269 | `; 270 | }; 271 | 272 | export const resetPassword = (url, appName) => { 273 | return ` 274 | 276 | 277 | 278 | 279 | 280 | 281 | Password Rest 282 | 468 | 469 | 470 | 471 | 472 | 473 | 534 | 535 |
474 | 475 | 476 | 477 | 480 | 481 | 482 | 483 | 518 | 519 | 520 | 531 | 532 | 533 |
536 | 537 | 538 | 539 | `; 540 | }; 541 | 542 | export default { verifyEmail, resetPassword }; 543 | -------------------------------------------------------------------------------- /src/services/jwtService.js: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import jwt from 'jsonwebtoken'; 3 | import moment from 'moment'; 4 | import APIError from '~/utils/apiError'; 5 | 6 | export const sign = async (userId, expires, secret, options) => { 7 | try { 8 | const payload = { 9 | sub: userId, 10 | iat: moment().unix(), 11 | exp: expires.unix() 12 | }; 13 | return jwt.sign(payload, secret, options); 14 | } catch (err) { 15 | throw new APIError(err.message, httpStatus.UNAUTHORIZED); 16 | } 17 | }; 18 | 19 | export const verify = async (token, secret, options) => { 20 | try { 21 | return jwt.verify(token, secret, options); 22 | } catch (err) { 23 | throw new APIError(err.message, httpStatus.UNAUTHORIZED); 24 | } 25 | }; 26 | 27 | export default { sign, verify }; 28 | -------------------------------------------------------------------------------- /src/services/tokenService.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import config from '~/config/config'; 3 | import APIError from '~/utils/apiError'; 4 | import User from '~/models/userModel'; 5 | import Token from '~/models/tokenModel'; 6 | import jwtService from './jwtService'; 7 | import httpStatus from 'http-status'; 8 | import crypto from 'crypto'; 9 | 10 | export const generateRandomToken = async (length = 66) => { 11 | const random = crypto.randomBytes(length).toString('hex'); 12 | return random; 13 | }; 14 | 15 | export const verifyToken = async (token, type) => { 16 | const tokenDoc = await Token.findOne({ token, type, blacklisted: false }); 17 | if (!tokenDoc) { 18 | throw new APIError('Token not found', httpStatus.UNAUTHORIZED); 19 | } 20 | if (moment(tokenDoc.expiresAt).format() < moment().format()) { 21 | throw new APIError('Token expires', httpStatus.UNAUTHORIZED); 22 | } 23 | return tokenDoc; 24 | }; 25 | 26 | export const generateAuthTokens = async (user) => { 27 | const accessTokenExpires = moment().add(config.JWT_ACCESS_TOKEN_EXPIRATION_MINUTES, 'minutes'); 28 | const accessToken = await jwtService.sign(user.id, accessTokenExpires, config.JWT_ACCESS_TOKEN_SECRET_PRIVATE, { 29 | algorithm: 'RS256' 30 | }); 31 | 32 | const refreshTokenExpires = moment().add(config.REFRESH_TOKEN_EXPIRATION_DAYS, 'days'); 33 | const refreshToken = await generateRandomToken(); 34 | await Token.saveToken(refreshToken, user.id, refreshTokenExpires.format(), config.TOKEN_TYPES.REFRESH); 35 | 36 | return { 37 | accessToken: { 38 | token: accessToken, 39 | expires: accessTokenExpires.format() 40 | }, 41 | refreshToken: { 42 | token: refreshToken, 43 | expires: refreshTokenExpires.format() 44 | } 45 | }; 46 | }; 47 | 48 | export const generateVerifyEmailToken = async (user) => { 49 | const expires = moment().add(config.VERIFY_EMAIL_TOKEN_EXPIRATION_MINUTES, 'minutes'); 50 | const verifyEmailToken = await generateRandomToken(); 51 | await Token.saveToken(verifyEmailToken, user.id, expires, config.TOKEN_TYPES.VERIFY_EMAIL); 52 | return verifyEmailToken; 53 | }; 54 | 55 | export const generateResetPasswordToken = async (email) => { 56 | const user = await User.getUserByEmail(email); 57 | if (!user) { 58 | throw new APIError('No users found with this email', httpStatus.NOT_FOUND); 59 | } 60 | const expires = moment().add(config.RESET_PASSWORD_TOKEN_EXPIRATION_MINUTES, 'minutes'); 61 | const resetPasswordToken = await generateRandomToken(); 62 | await Token.saveToken(resetPasswordToken, user.id, expires, config.TOKEN_TYPES.RESET_PASSWORD); 63 | return resetPasswordToken; 64 | }; 65 | 66 | export default { 67 | generateRandomToken, 68 | verifyToken, 69 | generateAuthTokens, 70 | generateVerifyEmailToken, 71 | generateResetPasswordToken 72 | }; 73 | -------------------------------------------------------------------------------- /src/utils/apiError.js: -------------------------------------------------------------------------------- 1 | class APIError extends Error { 2 | constructor(message, status, isOperational = true) { 3 | super(message); 4 | this.name = this.constructor.name; 5 | this.message = message; 6 | this.status = status; 7 | this.isOperational = isOperational; 8 | Error.captureStackTrace(this, this.constructor); 9 | } 10 | } 11 | 12 | export default APIError; 13 | -------------------------------------------------------------------------------- /src/utils/catchAsync.js: -------------------------------------------------------------------------------- 1 | const catchAsync = (fn) => (req, res, next) => { 2 | Promise.resolve(fn(req, res, next)).catch((err) => next(err)); 3 | }; 4 | 5 | export default catchAsync; 6 | -------------------------------------------------------------------------------- /src/utils/resizeImage.js: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | 3 | const ResizeImage = async (folder, fileName, options = { width: 300, height: 300 }) => { 4 | const newFileName = `${options.width}x${options.height}-` + fileName; 5 | await sharp(folder + '/' + fileName) 6 | .resize(options.width, options.height) 7 | .toFile(folder + '/' + newFileName); 8 | return newFileName; 9 | }; 10 | 11 | export default ResizeImage; 12 | -------------------------------------------------------------------------------- /src/validations/authValidation.js: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export const signup = { 4 | body: Joi.object().keys({ 5 | firstName: Joi.string().trim().min(2).max(66).required(), 6 | lastName: Joi.string().trim().min(2).max(66).required(), 7 | userName: Joi.string().alphanum().min(6).max(66).required(), 8 | email: Joi.string().email().required(), 9 | password: Joi.string().trim().min(6).max(666).required() 10 | }) 11 | }; 12 | 13 | export const signin = { 14 | body: Joi.object().keys({ 15 | userName: Joi.string().required(), 16 | password: Joi.string().required() 17 | }) 18 | }; 19 | 20 | export const signout = { 21 | body: Joi.object().keys({ 22 | refreshToken: Joi.string().required() 23 | }) 24 | }; 25 | 26 | export const refreshTokens = { 27 | body: Joi.object().keys({ 28 | refreshToken: Joi.string().required() 29 | }) 30 | }; 31 | 32 | export const forgotPassword = { 33 | body: Joi.object().keys({ 34 | email: Joi.string().email().required() 35 | }) 36 | }; 37 | 38 | export const resetPassword = { 39 | query: Joi.object().keys({ 40 | token: Joi.string().required() 41 | }), 42 | body: Joi.object().keys({ 43 | password: Joi.string().trim().min(6).max(666).required() 44 | }) 45 | }; 46 | 47 | export const verifyEmail = { 48 | query: Joi.object().keys({ 49 | token: Joi.string().required() 50 | }) 51 | }; 52 | 53 | export const updateMe = { 54 | body: Joi.object().keys({ 55 | firstName: Joi.string().trim().min(2).max(66), 56 | lastName: Joi.string().trim().min(2).max(66), 57 | userName: Joi.string().alphanum().min(6).max(66), 58 | email: Joi.string().email(), 59 | password: Joi.string().trim().min(6).max(666), 60 | avatar: Joi.string().max(666) 61 | }) 62 | }; 63 | 64 | export default { 65 | signup, 66 | signin, 67 | updateMe, 68 | signout, 69 | refreshTokens, 70 | verifyEmail, 71 | forgotPassword, 72 | resetPassword 73 | }; 74 | -------------------------------------------------------------------------------- /src/validations/customValidation.js: -------------------------------------------------------------------------------- 1 | export const mongoId = (value, helpers) => { 2 | if (!value.match(/^(0x|0h)?[0-9A-F]{24}$/i)) { 3 | return helpers.message('{{#label}} must be a valid mongo id'); 4 | } 5 | return value; 6 | }; 7 | -------------------------------------------------------------------------------- /src/validations/roleValidation.js: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { mongoId } from './customValidation'; 3 | 4 | export const createRole = { 5 | body: Joi.object().keys({ 6 | name: Joi.string().trim().min(2).max(66).required(), 7 | description: Joi.string().min(2).max(666).allow(''), 8 | permissions: Joi.array().items(Joi.string().custom(mongoId)).unique() 9 | }) 10 | }; 11 | 12 | export const updateRole = { 13 | params: Joi.object().keys({ 14 | roleId: Joi.string().custom(mongoId).required() 15 | }), 16 | body: Joi.object().keys({ 17 | name: Joi.string().trim().min(2).max(66), 18 | description: Joi.string().min(2).max(666).allow(''), 19 | permissions: Joi.array().items(Joi.string().custom(mongoId)).unique() 20 | }) 21 | }; 22 | 23 | export const deleteRole = { 24 | params: Joi.object().keys({ 25 | roleId: Joi.string().custom(mongoId) 26 | }) 27 | }; 28 | 29 | export const getRoles = { 30 | query: Joi.object().keys({ 31 | q: Joi.string(), 32 | sortBy: Joi.string(), 33 | sortDirection: Joi.string(), 34 | limit: Joi.number().integer(), 35 | page: Joi.number().integer() 36 | }) 37 | }; 38 | 39 | export const getRole = { 40 | params: Joi.object().keys({ 41 | roleId: Joi.string().custom(mongoId) 42 | }) 43 | }; 44 | 45 | export default { createRole, getRole, updateRole, getRoles, deleteRole }; 46 | -------------------------------------------------------------------------------- /src/validations/userValidation.js: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { mongoId } from './customValidation'; 3 | 4 | export const createUser = { 5 | body: Joi.object().keys({ 6 | firstName: Joi.string().trim().min(2).max(66).required(), 7 | lastName: Joi.string().trim().min(2).max(66).required(), 8 | userName: Joi.string().alphanum().min(6).max(66).required(), 9 | email: Joi.string().required().email(), 10 | password: Joi.string().trim().min(6).max(666).required(), 11 | roles: Joi.array().items(Joi.string().custom(mongoId)).min(1).max(6).unique().required(), 12 | avatar: Joi.string().max(666) 13 | }) 14 | }; 15 | 16 | export const getUsers = { 17 | query: Joi.object().keys({ 18 | q: Joi.string(), 19 | sortBy: Joi.string(), 20 | sortDirection: Joi.string(), 21 | limit: Joi.number().integer(), 22 | page: Joi.number().integer() 23 | }) 24 | }; 25 | 26 | export const getUser = { 27 | params: Joi.object().keys({ 28 | userId: Joi.string().custom(mongoId) 29 | }) 30 | }; 31 | 32 | export const updateUser = { 33 | params: Joi.object().keys({ 34 | userId: Joi.string().custom(mongoId).required() 35 | }), 36 | body: Joi.object().keys({ 37 | firstName: Joi.string().trim().min(2).max(66), 38 | lastName: Joi.string().trim().min(2).max(66), 39 | userName: Joi.string().alphanum().min(6).max(66), 40 | email: Joi.string().email(), 41 | password: Joi.string().trim().min(6).max(666), 42 | roles: Joi.array().items(Joi.string().custom(mongoId)).min(1).max(6).unique(), 43 | avatar: Joi.string().max(666) 44 | }) 45 | }; 46 | 47 | export const deleteUser = { 48 | params: Joi.object().keys({ 49 | userId: Joi.string().custom(mongoId) 50 | }) 51 | }; 52 | 53 | export default { createUser, getUsers, getUser, updateUser, deleteUser }; 54 | --------------------------------------------------------------------------------