├── .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 |
203 |
204 |
205 |
206 |
207 | ${appName}
208 | |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | Verify your email address
218 | Thanks for signing up for ${appName}! We're excited to have you as an early user.
219 |
220 |
221 |
223 |
224 |
225 |
229 | |
230 |
231 |
232 | Thanks, The ${appName}
233 |
234 |
235 |
236 |
237 | If you’re having trouble clicking the button, copy
238 | and paste the URL below into your web browser.
239 |
240 | ${url}
241 | |
242 |
243 |
244 | |
245 |
246 |
247 | |
248 |
249 |
250 |
251 |
260 | |
261 |
262 |
263 | |
264 |
265 |
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 |
474 |
475 |
476 |
477 |
478 | ${appName}
479 | |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 | Password Reset
489 | We received a request to update your password.
490 |
491 |
493 |
494 |
495 |
499 | |
500 |
501 |
502 | Thanks, The ${appName}
503 |
504 |
505 |
506 |
507 | If you’re having trouble clicking the button, copy
508 | and paste the URL below into your web browser.
509 |
510 | ${url}
511 | |
512 |
513 |
514 | |
515 |
516 |
517 | |
518 |
519 |
520 |
521 |
530 | |
531 |
532 |
533 | |
534 |
535 |
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 |
--------------------------------------------------------------------------------