├── .editorconfig ├── .env.example ├── .env.test ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .lintstagedrc.json ├── .prettierrc ├── .runtime └── banner.js ├── .vscode ├── cSpell.json ├── extensions.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── ecosystem.config.json ├── package.json ├── src ├── apis │ ├── controllers │ │ ├── auth.controller.js │ │ ├── index.js │ │ └── task.controller.js │ ├── models │ │ ├── index.js │ │ ├── plugins │ │ │ ├── index.js │ │ │ ├── paginate.plugin.js │ │ │ └── toJson.plugin.js │ │ ├── task.model.js │ │ ├── token.model.js │ │ └── user.model.js │ ├── plugins │ │ └── passport.js │ ├── routes │ │ ├── index.js │ │ ├── v1 │ │ │ ├── auth.route.js │ │ │ ├── task.route.js │ │ │ └── user.route.js │ │ └── v2 │ │ │ └── .gitkeep │ ├── services │ │ ├── auth.service.js │ │ ├── index.js │ │ ├── task.service.js │ │ ├── token.service.js │ │ └── user.service.js │ └── validations │ │ ├── auth.validation.js │ │ ├── customize.validation.js │ │ └── index.js ├── app.js ├── configs │ ├── env.js │ └── tokens.js ├── docs │ └── components.yml ├── libs │ ├── banner │ │ └── index.js │ ├── logger │ │ └── index.js │ └── os.js ├── loaders │ ├── expressLoader.js │ ├── mongooseLoader.js │ ├── monitorLoader.js │ ├── passportLoader.js │ ├── publicLoader.js │ ├── swaggerLoader.js │ └── winstonLoader.js ├── middlewares │ ├── error.js │ ├── rate-limit.js │ └── validate.js ├── public │ ├── .gitkeep │ ├── favicon.ico │ └── vendors │ │ ├── chart.bundle.min.js │ │ └── socket.io.min.js └── utils │ ├── api-error.js │ ├── catch-async.js │ └── pick-keys.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # @w3tec 2 | # http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | # Use 2 spaces since npm does not respect custom indentation settings 18 | [package.json] 19 | indent_style = space 20 | indent_size = 4 21 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 2 | # APPLICATION 3 | # 4 | APP_NAME=nodejs-api-boilerplate 5 | APP_SCHEMA=http 6 | APP_HOST=localhost 7 | APP_PORT=3000 8 | APP_ROUTE_PREFIX=/api 9 | APP_BANNER=true 10 | 11 | # 12 | # LOGGING 13 | # 14 | LOG_LEVEL=debug 15 | LOG_OUTPUT=dev 16 | 17 | # 18 | # DATABASE 19 | # 20 | DB_CONNECTION=mongodb://localhost/example 21 | DB_DATABASE=mongodb 22 | DB_LOGGING=error 23 | DB_LOGGER=advanced-console 24 | 25 | # 26 | # PASSPORT 27 | # 28 | PASSPORT_JWT=jwt_example 29 | PASSPORT_JWT_ACCESS_EXPIRED=900 30 | PASSPORT_JWT_REFRESH_EXPIRED=259200 31 | 32 | # 33 | # Swagger 34 | # 35 | SWAGGER_ENABLED=true 36 | SWAGGER_ROUTE=/swagger 37 | SWAGGER_USERNAME=admin 38 | SWAGGER_PASSWORD=1234 39 | 40 | # 41 | # Status Monitor 42 | # 43 | MONITOR_ENABLED=true 44 | MONITOR_ROUTE=/monitor 45 | MONITOR_USERNAME=admin 46 | MONITOR_PASSWORD=1234 47 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # 2 | # APPLICATION 3 | # 4 | APP_NAME=nodejs-api-boilerplate 5 | APP_SCHEMA=http 6 | APP_HOST=localhost 7 | APP_PORT=3000 8 | APP_ROUTE_PREFIX=/api 9 | APP_BANNER=true 10 | 11 | # 12 | # LOGGING 13 | # 14 | LOG_LEVEL=none 15 | LOG_OUTPUT=dev 16 | 17 | # 18 | # DATABASE 19 | # 20 | DB_CONNECTION=mongodb://localhost/testing 21 | DB_DATABASE=mongodb 22 | DB_LOGGING=error 23 | DB_LOGGER=advanced-console 24 | 25 | # 26 | # PASSPORT 27 | # 28 | PASSPORT_JWT=jwt_example 29 | PASSPORT_JWT_ACCESS_EXPIRED=900 30 | PASSPORT_JWT_REFRESH_EXPIRED=259200 31 | 32 | # 33 | # Swagger 34 | # 35 | SWAGGER_ENABLED=true 36 | SWAGGER_ROUTE=/swagger 37 | SWAGGER_USERNAME=admin 38 | SWAGGER_PASSWORD=1234 39 | 40 | # 41 | # Status Monitor 42 | # 43 | MONITOR_ENABLED=true 44 | MONITOR_ROUTE=/monitor 45 | MONITOR_USERNAME=admin 46 | MONITOR_PASSWORD=1234 47 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "jest": true 5 | }, 6 | "extends": ["airbnb-base", "plugin:jest/recommended", "plugin:security/recommended", "plugin:prettier/recommended"], 7 | "plugins": ["jest", "security", "prettier"], 8 | "parserOptions": { 9 | "ecmaVersion": 2018 10 | }, 11 | "rules": { 12 | // "no-console": "error", 13 | "func-names": "off", 14 | "no-underscore-dangle": "off", 15 | "consistent-return": "off", 16 | "jest/expect-expect": "off", 17 | "security/detect-object-injection": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Convert text file line endings to lf 2 | * text eol=lf 3 | *.js text 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs # 2 | /logs 3 | *.log 4 | *.log* 5 | 6 | # Node files # 7 | node_modules/ 8 | npm-debug.log 9 | yarn-error.log 10 | .env 11 | 12 | # OS generated files # 13 | .DS_Store 14 | Thumbs.db 15 | .tmp/ 16 | 17 | # Dist # 18 | dist/ 19 | 20 | # IDE # 21 | .idea/ 22 | *.swp 23 | .awcache 24 | 25 | # Generated source-code # 26 | coverage/ 27 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": "eslint" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 125, 6 | "endOfLine": "lf" 7 | } 8 | -------------------------------------------------------------------------------- /.runtime/banner.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const figlet = require('figlet') 3 | 4 | figlet.text(process.argv[2], (error, data) => { 5 | if (error) { 6 | return process.exit(1) 7 | } 8 | 9 | console.log(chalk.blue(data)) 10 | console.log('') 11 | return process.exit(0) 12 | }) 13 | -------------------------------------------------------------------------------- /.vscode/cSpell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1", 3 | "language": "en", 4 | "words": [ 5 | "autorestart", 6 | "hsts", 7 | "mongodb", 8 | "openapi", 9 | "prefs", 10 | "Sheeh" 11 | ], 12 | "flagWords": [ 13 | "hte" 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "streetsidesoftware.code-spell-checker", 4 | "EditorConfig.EditorConfig", 5 | "christian-kohler.path-intellisense", 6 | "mikestead.dotenv", 7 | "Orta.vscode-jest" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.enabled": true, 3 | "files.exclude": {}, 4 | "importSorter.generalConfiguration.sortOnBeforeSave": true, 5 | "files.trimTrailingWhitespace": true, 6 | "editor.formatOnSave": true 7 | } 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off, thank you so much for taking the time to contribute. All contributions are more than welcome! 4 | 5 | ## How can I contribute? 6 | 7 | If you have an awesome new feature that you want to implement or you found a bug that you would like to fix, here are some instructions to guide you through the process: 8 | 9 | - **Create an issue** to explain and discuss the details 10 | - **Fork the repo** 11 | - **Clone the repo** and set it up (check out the [manual installation](https://github.com/trantoan960/nodejs-api-boilerplate#-getting-started) section in README.md) 12 | - **Implement** the necessary changes 13 | - **Create tests** to keep the code coverage high 14 | - **Send a pull request** 15 | 16 | ## Guidelines 17 | 18 | ### Git commit messages 19 | 20 | - Limit the subject line to 72 characters 21 | - Capitalize the first letter of the subject line 22 | - Use the present tense ("Add feature" instead of "Added feature") 23 | - Separate the subject from the body with a blank line 24 | - Reference issues and pull requests in the body 25 | 26 | ### Coding style guide 27 | 28 | We are using ESLint to ensure a consistent code style in the project, based on [Airbnb's JS style guide](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb-base). 29 | 30 | Some other ESLint plugins are also being used, such as the [Prettier](https://github.com/prettier/eslint-plugin-prettier) and [Jest](https://github.com/jest-community/eslint-plugin-jest) plugins. 31 | 32 | Please make sure that the code you are pushing conforms to the style guides mentioned above. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sky Albert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | RHP Team 3 |

4 | 5 |

Nodejs API Boilerplate

6 | 7 |

8 | 9 | travis 10 | 11 | 12 | appveyor 13 | 14 | 15 | StackShare 16 | 17 |

18 | 19 |

20 | A delightful way to building a Node.js RESTful API Services with beautiful code written in Vanilla Javascript.
21 | Inspired by the awesome framework & other repo(s) on Github, Gitlab, Gitee,...
22 | Made with ❤️ by Tran Toan 23 |

24 | 25 |
26 | 27 | ## ❯ Why I should be use it 28 | 29 | My main goal with this project is a feature complete server application. 30 | I like you to be focused on your business and not spending hours in project configuration. 31 | 32 | Try it!! I'm happy to hear your feedback or any kind of new features. 33 | 34 | ### Features 35 | 36 | - [x] **NoSQL database**: [MongoDB](https://www.mongodb.com) object data modeling using [Mongoose](https://mongoosejs.com) 37 | - [x] **API documentation**: with [swagger-jsdoc](https://github.com/Surnet/swagger-jsdoc) and [swagger-ui-express](https://github.com/scottie1984/swagger-ui-express) 38 | - [x] **Process management**: advanced production process management using [PM2](https://pm2.keymetrics.io) 39 | - [x] **Dependency management**: with [Yarn](https://yarnpkg.com) 40 | - [x] **Environment variables**: using [dotenv](https://github.com/motdotla/dotenv) and [cross-env](https://github.com/kentcdodds/cross-env#readme) 41 | - [x] **Security**: set security HTTP headers using [helmet](https://helmetjs.github.io) 42 | - [x] **Santizing**: sanitize request data against xss and query injection 43 | - [x] **CORS**: Cross-Origin Resource-Sharing enabled using [cors](https://github.com/expressjs/cors) 44 | - [ ] **Code coverage**: using [coveralls](https://coveralls.io) 45 | - [ ] **Code quality**: with [Codacy](https://www.codacy.com) 46 | - [x] **Git hooks**: with [husky](https://github.com/typicode/husky) and [lint-staged](https://github.com/okonet/lint-staged) 47 | - [x] **Linting**: with [ESLint](https://eslint.org) and [Prettier](https://prettier.io) 48 | - [x] **Editor config**: consistent editor configuration using [EditorConfig](https://editorconfig.org) 49 | - [x] **Authentication and authorization**: using [passport](http://www.passportjs.org) 50 | - [x] **Validation**: request data validation using [Joi](https://github.com/hapijs/joi) 51 | - [x] **Logging**: using [winston](https://github.com/winstonjs/winston) and [morgan](https://github.com/expressjs/morgan) 52 | - [ ] **Testing**: unit and integration tests using [Jest](https://jestjs.io) 53 | - [x] **Error handling**: centralized error handling mechanism 54 | - [ ] **Compression**: gzip compression with [compression](https://github.com/expressjs/compression) 55 | - [ ] **CI**: continuous integration with [Travis CI](https://travis-ci.org) 56 | - [ ] **Docker support** 57 | 58 | ## ❯ Getting Started 59 | 60 | ### Step 1: Set up the Development Environment 61 | 62 | You need to set up your development environment before you can do anything. 63 | 64 | Install [Node.js and NPM](https://nodejs.org/en/download/) 65 | 66 | - on OSX use [homebrew](http://brew.sh) `brew install node` 67 | - on Windows use [chocolatey](https://chocolatey.org/) `choco install nodejs` 68 | 69 | Install yarn globally 70 | 71 | ```bash 72 | npm install --global yarn 73 | ``` 74 | 75 | ### Step 2: Create new Project 76 | 77 | Fork or download this project. Configure your package.json for your new project. 78 | 79 | Then copy the `.env.example` file and rename it to `.env`. In this file you have to add your database connection information. 80 | 81 | Create a new database with the name you have in your `.env`-file. 82 | 83 | Then setup your application environment. 84 | 85 | ```bash 86 | yarn install 87 | ``` 88 | 89 | > This installs all dependencies with yarn. After that your development environment is ready to use. 90 | 91 | ### Step 3: Serve your App 92 | 93 | Go to the project dir and start your app with this yarn script. 94 | 95 | ```bash 96 | yarn dev 97 | ``` 98 | 99 | > This starts a local server using `nodemon`, which will watch for any file changes and will restart the server according to these changes. 100 | > The server address will be displayed to you as `http://localhost:3000` [default]. 101 | 102 | ## ❯ Contributing 103 | 104 | Contributions are more than welcome! Please check out the [contributing guide](CONTRIBUTING.md). 105 | 106 | ## ❯ License 107 | 108 | [MIT](LICENSE) 109 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignores: [(commit) => commit.includes('init')], 3 | extends: ['@commitlint/config-conventional'], 4 | rules: { 5 | 'body-leading-blank': [2, 'always'], 6 | 'footer-leading-blank': [1, 'always'], 7 | 'header-max-length': [2, 'always', 108], 8 | 'subject-empty': [2, 'never'], 9 | 'type-empty': [2, 'never'], 10 | 'type-enum': [ 11 | 2, 12 | 'always', 13 | [ 14 | 'feat', 15 | 'fix', 16 | 'perf', 17 | 'style', 18 | 'docs', 19 | 'test', 20 | 'refactor', 21 | 'build', 22 | 'ci', 23 | 'chore', 24 | 'revert', 25 | 'wip', 26 | 'workflow', 27 | 'types', 28 | 'release', 29 | ], 30 | ], 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /ecosystem.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "nodejs-api-boilerplate-app", 5 | "script": "src/app.js", 6 | "instances": 1, 7 | "autorestart": true, 8 | "watch": true, 9 | "time": true, 10 | "env": { 11 | "NODE_ENV": "production" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-api-boilerplate", 3 | "description": "A delightful way to building a Node.js RESTful API Services with beautiful code written in Vanilla Javascript", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "author": "Sky Albert ", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "cross-env NODE_ENV=development nodemon src/app.js", 10 | "start": "cross-env NODE_ENV=development node src/app.js" 11 | }, 12 | "dependencies": { 13 | "bcryptjs": "^2.4.3", 14 | "chalk": "^4.1.2", 15 | "compression": "^1.7.4", 16 | "cors": "^2.8.5", 17 | "cross-env": "^7.0.3", 18 | "dotenv": "^10.0.0", 19 | "express": "^4.17.1", 20 | "express-basic-auth": "^1.2.0", 21 | "express-mongo-sanitize": "^2.1.0", 22 | "express-rate-limit": "^5.3.0", 23 | "express-status-monitor": "trantoan960/express-status-monitor", 24 | "figlet": "^1.5.2", 25 | "helmet": "^4.6.0", 26 | "http-status": "^1.5.0", 27 | "husky": "^7.0.2", 28 | "joi": "^17.4.2", 29 | "jsonwebtoken": "^8.5.1", 30 | "moment": "^2.29.1", 31 | "mongoose": "^6.0.2", 32 | "morgan": "^1.10.0", 33 | "passport": "^0.4.1", 34 | "passport-jwt": "^4.0.0", 35 | "serve-favicon": "^2.5.0", 36 | "swagger-jsdoc": "^6.1.0", 37 | "swagger-ui-express": "^4.1.6", 38 | "validator": "^13.6.0", 39 | "winston": "^3.3.3", 40 | "xss-clean": "^0.1.1" 41 | }, 42 | "devDependencies": { 43 | "@commitlint/cli": "^13.1.0", 44 | "@commitlint/config-conventional": "^13.1.0", 45 | "eslint": "^7.32.0", 46 | "eslint-config-prettier": "^8.3.0", 47 | "eslint-plugin-jest": "^24.4.0", 48 | "eslint-plugin-prettier": "^3.4.1", 49 | "eslint-plugin-security": "^1.4.0", 50 | "nodemon": "^2.0.12", 51 | "prettier": "^2.3.2" 52 | }, 53 | "lint-staged": { 54 | "*.{js,json}": "eslint --fix" 55 | }, 56 | "husky": { 57 | "hooks": { 58 | "pre-commit": "prettier --write . && git add -A .", 59 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/apis/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require('http-status') 2 | 3 | const catchAsync = require('../../utils/catch-async') 4 | const { authService, tokenService, userService } = require('../services') 5 | 6 | const register = catchAsync(async (req, res) => { 7 | const user = await userService.createUser(req.body) 8 | const tokens = await tokenService.generateAuthTokens(user) 9 | res.status(httpStatus.CREATED).send({ user, tokens }) 10 | }) 11 | 12 | const login = catchAsync(async (req, res) => { 13 | const { email, password } = req.body 14 | const user = await authService.loginUserWithEmailAndPassword(email, password) 15 | const tokens = await tokenService.generateAuthTokens(user) 16 | res.send({ user, tokens }) 17 | }) 18 | 19 | const logout = catchAsync(async (req, res) => { 20 | await authService.logout(req.body.refreshToken) 21 | res.status(httpStatus.NO_CONTENT).send() 22 | }) 23 | 24 | const refreshTokens = catchAsync(async (req, res) => { 25 | const tokens = await authService.refreshAuth(req.body.refreshToken) 26 | res.send({ ...tokens }) 27 | }) 28 | 29 | const forgotPassword = catchAsync(async (req, res) => { 30 | const resetPasswordToken = await tokenService.generateResetPasswordToken(req.body.email) 31 | await emailService.sendResetPasswordEmail(req.body.email, resetPasswordToken) 32 | res.status(httpStatus.NO_CONTENT).send() 33 | }) 34 | 35 | const resetPassword = catchAsync(async (req, res) => { 36 | await authService.resetPassword(req.query.token, req.body.password) 37 | res.status(httpStatus.NO_CONTENT).send() 38 | }) 39 | 40 | const sendVerificationEmail = catchAsync(async (req, res) => { 41 | const verifyEmailToken = await tokenService.generateVerifyEmailToken(req.user) 42 | await emailService.sendVerificationEmail(req.user.email, verifyEmailToken) 43 | res.status(httpStatus.NO_CONTENT).send() 44 | }) 45 | 46 | const verifyEmail = catchAsync(async (req, res) => { 47 | await authService.verifyEmail(req.query.token) 48 | res.status(httpStatus.NO_CONTENT).send() 49 | }) 50 | 51 | module.exports = { 52 | register, 53 | login, 54 | logout, 55 | refreshTokens, 56 | forgotPassword, 57 | resetPassword, 58 | sendVerificationEmail, 59 | verifyEmail, 60 | } 61 | -------------------------------------------------------------------------------- /src/apis/controllers/index.js: -------------------------------------------------------------------------------- 1 | module.exports.authController = require('./auth.controller') 2 | module.exports.taskController = require('./task.controller') 3 | -------------------------------------------------------------------------------- /src/apis/controllers/task.controller.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require('http-status') 2 | 3 | const catchAsync = require('../../utils/catch-async') 4 | const { taskService } = require('../services') 5 | 6 | const CreateTask = catchAsync(async (req, res) => { 7 | const task = await taskService.createTask(req.body) 8 | res.status(httpStatus.CREATED).send({ task }) 9 | }) 10 | 11 | const DeleteTask = catchAsync(async (req, res) => { 12 | await taskService.deleteTask(req.params.id) 13 | res.status(httpStatus.NO_CONTENT).send() 14 | }) 15 | 16 | const GetTasks = catchAsync(async (req, res) => { 17 | const tasks = await taskService.getTasks() 18 | res.status(httpStatus.OK).send({ tasks }) 19 | }) 20 | 21 | const UpdateTask = catchAsync(async (req, res) => { 22 | const task = await taskService.updateTask(req.params.id, req.body) 23 | res.status(httpStatus.OK).send({ task }) 24 | }) 25 | 26 | module.exports = { 27 | CreateTask, 28 | DeleteTask, 29 | GetTasks, 30 | UpdateTask, 31 | } 32 | -------------------------------------------------------------------------------- /src/apis/models/index.js: -------------------------------------------------------------------------------- 1 | module.exports.Task = require('./task.model') 2 | module.exports.Token = require('./token.model') 3 | module.exports.User = require('./user.model') 4 | -------------------------------------------------------------------------------- /src/apis/models/plugins/index.js: -------------------------------------------------------------------------------- 1 | module.exports.toJSON = require('./toJson.plugin') 2 | module.exports.paginate = require('./paginate.plugin') 3 | -------------------------------------------------------------------------------- /src/apis/models/plugins/paginate.plugin.js: -------------------------------------------------------------------------------- 1 | const paginate = (schema) => { 2 | /** 3 | * @typedef {Object} QueryResult 4 | * @property {Document[]} results - Results found 5 | * @property {number} page - Current page 6 | * @property {number} limit - Maximum number of results per page 7 | * @property {number} totalPages - Total number of pages 8 | * @property {number} totalResults - Total number of documents 9 | */ 10 | /** 11 | * Query for documents with pagination 12 | * @param {Object} [filter] - Mongo filter 13 | * @param {Object} [options] - Query options 14 | * @param {string} [options.sortBy] - Sorting criteria using the format: sortField:(desc|asc). Multiple sorting criteria should be separated by commas (,) 15 | * @param {string} [options.populate] - Populate data fields. Hierarchy of fields should be separated by (.). Multiple populating criteria should be separated by commas (,) 16 | * @param {number} [options.limit] - Maximum number of results per page (default = 10) 17 | * @param {number} [options.page] - Current page (default = 1) 18 | * @returns {Promise} 19 | */ 20 | schema.statics.paginate = async function (filter, options) { 21 | let sort = '' 22 | if (options.sortBy) { 23 | const sortingCriteria = [] 24 | options.sortBy.split(',').forEach((sortOption) => { 25 | const [key, order] = sortOption.split(':') 26 | sortingCriteria.push((order === 'desc' ? '-' : '') + key) 27 | }) 28 | sort = sortingCriteria.join(' ') 29 | } else { 30 | sort = 'createdAt' 31 | } 32 | 33 | const limit = options.limit && parseInt(options.limit, 10) > 0 ? parseInt(options.limit, 10) : 10 34 | const page = options.page && parseInt(options.page, 10) > 0 ? parseInt(options.page, 10) : 1 35 | const skip = (page - 1) * limit 36 | 37 | const countPromise = this.countDocuments(filter).exec() 38 | let docsPromise = this.find(filter).sort(sort).skip(skip).limit(limit) 39 | 40 | if (options.populate) { 41 | options.populate.split(',').forEach((populateOption) => { 42 | docsPromise = docsPromise.populate( 43 | populateOption 44 | .split('.') 45 | .reverse() 46 | .reduce((a, b) => ({ path: b, populate: a })) 47 | ) 48 | }) 49 | } 50 | 51 | docsPromise = docsPromise.exec() 52 | 53 | return Promise.all([countPromise, docsPromise]).then((values) => { 54 | const [totalResults, results] = values 55 | const totalPages = Math.ceil(totalResults / limit) 56 | const result = { 57 | results, 58 | page, 59 | limit, 60 | totalPages, 61 | totalResults, 62 | } 63 | return Promise.resolve(result) 64 | }) 65 | } 66 | } 67 | 68 | module.exports = paginate 69 | -------------------------------------------------------------------------------- /src/apis/models/plugins/toJson.plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A mongoose schema plugin which applies the following in the toJSON transform call: 3 | * - removes __v, createdAt, updatedAt, and any path that has private: true 4 | * - replaces _id with id 5 | */ 6 | 7 | const deleteAtPath = (obj, path, index) => { 8 | if (index === path.length - 1) { 9 | delete obj[path[index]] 10 | return 11 | } 12 | deleteAtPath(obj[path[index]], path, index + 1) 13 | } 14 | 15 | const toJSON = (schema) => { 16 | let transform 17 | if (schema.options.toJSON && schema.options.toJSON.transform) { 18 | transform = schema.options.toJSON.transform 19 | } 20 | 21 | schema.options.toJSON = Object.assign(schema.options.toJSON || {}, { 22 | transform(doc, ret, options) { 23 | Object.keys(schema.paths).forEach((path) => { 24 | if (schema.paths[path].options && schema.paths[path].options.private) { 25 | deleteAtPath(ret, path.split('.'), 0) 26 | } 27 | }) 28 | 29 | ret.id = ret._id.toString() 30 | delete ret._id 31 | delete ret.__v 32 | delete ret.createdAt 33 | delete ret.updatedAt 34 | if (transform) { 35 | return transform(doc, ret, options) 36 | } 37 | }, 38 | }) 39 | } 40 | 41 | module.exports = toJSON 42 | -------------------------------------------------------------------------------- /src/apis/models/task.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const { toJSON } = require('./plugins') 4 | 5 | const taskSchema = mongoose.Schema( 6 | { 7 | title: { 8 | type: String, 9 | required: true, 10 | }, 11 | description: { 12 | type: String, 13 | required: false, 14 | }, 15 | dueDate: { 16 | type: Number, 17 | required: false, 18 | }, 19 | priority: { 20 | type: String, 21 | required: false, 22 | }, 23 | completed: { 24 | type: Boolean, 25 | required: true, 26 | default: false, 27 | }, 28 | }, 29 | { 30 | timestamps: true, 31 | } 32 | ) 33 | 34 | taskSchema.plugin(toJSON) 35 | 36 | /** 37 | * @typedef Token 38 | */ 39 | const Task = mongoose.model('Task', taskSchema) 40 | 41 | module.exports = Task 42 | -------------------------------------------------------------------------------- /src/apis/models/token.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const { toJSON } = require('./plugins') 4 | const { tokenTypes } = require('../../configs/tokens') 5 | 6 | const tokenSchema = mongoose.Schema( 7 | { 8 | token: { 9 | type: String, 10 | required: true, 11 | index: true, 12 | }, 13 | user: { 14 | type: mongoose.SchemaTypes.ObjectId, 15 | ref: 'User', 16 | required: true, 17 | }, 18 | type: { 19 | type: String, 20 | enum: [tokenTypes.REFRESH, tokenTypes.RESET_PASSWORD, tokenTypes.VERIFY_EMAIL], 21 | required: true, 22 | }, 23 | expires: { 24 | type: Date, 25 | required: true, 26 | }, 27 | blacklisted: { 28 | type: Boolean, 29 | default: false, 30 | }, 31 | }, 32 | { 33 | timestamps: true, 34 | } 35 | ) 36 | 37 | tokenSchema.plugin(toJSON) 38 | 39 | /** 40 | * @typedef Token 41 | */ 42 | const Token = mongoose.model('Token', tokenSchema) 43 | 44 | module.exports = Token 45 | -------------------------------------------------------------------------------- /src/apis/models/user.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const bcrypt = require('bcryptjs') 3 | const validator = require('validator') 4 | 5 | const { toJSON, paginate } = require('./plugins') 6 | 7 | const userSchema = mongoose.Schema( 8 | { 9 | displayName: { 10 | type: String, 11 | required: true, 12 | trim: true, 13 | }, 14 | email: { 15 | type: String, 16 | required: true, 17 | unique: true, 18 | trim: true, 19 | lowercase: true, 20 | validate(value) { 21 | if (!validator.isEmail(value)) { 22 | throw new Error('Invalid email') 23 | } 24 | }, 25 | }, 26 | password: { 27 | type: String, 28 | required: true, 29 | trim: true, 30 | minlength: 6, 31 | validate(value) { 32 | if (!value.match(/\d/) || !value.match(/[a-zA-Z]/)) { 33 | throw new Error('Password must contain at least one letter and one number') 34 | } 35 | }, 36 | private: true, 37 | }, 38 | isEmailVerified: { 39 | type: Boolean, 40 | default: false, 41 | }, 42 | }, 43 | { 44 | timestamps: true, 45 | } 46 | ) 47 | 48 | userSchema.plugin(toJSON) 49 | userSchema.plugin(paginate) 50 | 51 | /** 52 | * Check if email is taken 53 | * @param {string} email - The user's email 54 | * @param {ObjectId} [excludeUserId] - The id of the user to be excluded 55 | * @returns {Promise} 56 | */ 57 | userSchema.statics.isEmailTaken = async function (email, excludeUserId) { 58 | const user = await this.findOne({ email, _id: { $ne: excludeUserId } }) 59 | return !!user 60 | } 61 | 62 | /** 63 | * Check if password matches the user's password 64 | * @param {string} password 65 | * @returns {Promise} 66 | */ 67 | userSchema.methods.isPasswordMatch = async function (password) { 68 | const user = this 69 | return bcrypt.compare(password, user.password) 70 | } 71 | 72 | userSchema.pre('save', async function (next) { 73 | const user = this 74 | if (user.isModified('password')) { 75 | user.password = await bcrypt.hash(user.password, 10) 76 | } 77 | next() 78 | }) 79 | 80 | /** 81 | * @typedef User 82 | */ 83 | const User = mongoose.model('User', userSchema) 84 | 85 | module.exports = User 86 | -------------------------------------------------------------------------------- /src/apis/plugins/passport.js: -------------------------------------------------------------------------------- 1 | const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt') 2 | 3 | const env = require('../../configs/env') 4 | const { tokenTypes } = require('../../configs/tokens') 5 | 6 | const { User } = require('../models') 7 | 8 | const jwtOptions = { 9 | secretOrKey: env.passport.jwtToken, 10 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 11 | } 12 | 13 | const jwtVerify = async (payload, done) => { 14 | try { 15 | if (payload.type !== tokenTypes.ACCESS) { 16 | throw new Error('Invalid token type') 17 | } 18 | const user = await User.findById(payload.sub) 19 | if (!user) { 20 | return done(null, false) 21 | } 22 | done(null, user) 23 | } catch (error) { 24 | done(error, false) 25 | } 26 | } 27 | 28 | const jwtStrategy = new JwtStrategy(jwtOptions, jwtVerify) 29 | 30 | module.exports = { 31 | jwtStrategy, 32 | } 33 | -------------------------------------------------------------------------------- /src/apis/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | 3 | const authRoute = require('./v1/auth.route') 4 | const taskRoute = require('./v1/task.route') 5 | const userRoute = require('./v1/user.route') 6 | 7 | const router = express.Router() 8 | 9 | const defaultRoutes = [ 10 | { 11 | path: '/v1/auth', 12 | route: authRoute, 13 | }, 14 | { 15 | path: '/v1/tasks', 16 | route: taskRoute, 17 | }, 18 | { 19 | path: '/v1/users', 20 | route: userRoute, 21 | }, 22 | ] 23 | 24 | defaultRoutes.forEach((route) => { 25 | router.use(route.path, route.route) 26 | }) 27 | 28 | module.exports = router 29 | -------------------------------------------------------------------------------- /src/apis/routes/v1/auth.route.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | 3 | const { authController } = require('../../controllers') 4 | const { authValidation } = require('../../validations') 5 | 6 | const validate = require('../../../middlewares/validate') 7 | 8 | const router = express.Router() 9 | 10 | router.post('/login', validate(authValidation.loginSchema), authController.login) 11 | router.post('/logout', validate(authValidation.logoutSchema), authController.logout) 12 | router.post('/register', validate(authValidation.registerSchema), authController.register) 13 | 14 | module.exports = router 15 | 16 | /** 17 | * @swagger 18 | * tags: 19 | * name: Auth 20 | * description: Authentication 21 | */ 22 | 23 | /** 24 | * @swagger 25 | * /auth/register: 26 | * post: 27 | * summary: Register as user 28 | * tags: [Auth] 29 | * requestBody: 30 | * required: true 31 | * content: 32 | * application/json: 33 | * schema: 34 | * type: object 35 | * required: 36 | * - displayName 37 | * - email 38 | * - password 39 | * properties: 40 | * name: 41 | * type: string 42 | * email: 43 | * type: string 44 | * format: email 45 | * description: must be unique 46 | * password: 47 | * type: string 48 | * format: password 49 | * minLength: 6 50 | * description: At least one number and one letter 51 | * example: 52 | * name: fake name 53 | * email: fake@example.com 54 | * password: password1 55 | * responses: 56 | * "201": 57 | * description: Created 58 | * content: 59 | * application/json: 60 | * schema: 61 | * type: object 62 | * properties: 63 | * user: 64 | * $ref: '#/components/schemas/User' 65 | * tokens: 66 | * $ref: '#/components/schemas/AuthTokens' 67 | * "400": 68 | * $ref: '#/components/responses/DuplicateEmail' 69 | */ 70 | -------------------------------------------------------------------------------- /src/apis/routes/v1/task.route.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | 3 | const { taskController } = require('../../controllers') 4 | 5 | const router = express.Router() 6 | 7 | router.delete('/:id', taskController.DeleteTask) 8 | router.patch('/:id', taskController.UpdateTask) 9 | router.get('/list', taskController.GetTasks) 10 | router.post('/new', taskController.CreateTask) 11 | 12 | module.exports = router 13 | 14 | /** 15 | * @swagger 16 | * tags: 17 | * name: Tasks 18 | * description: Task management and retrieval 19 | */ 20 | -------------------------------------------------------------------------------- /src/apis/routes/v1/user.route.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | 3 | const router = express.Router() 4 | 5 | module.exports = router 6 | 7 | /** 8 | * @swagger 9 | * tags: 10 | * name: Users 11 | * description: User management and retrieval 12 | */ 13 | -------------------------------------------------------------------------------- /src/apis/routes/v2/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toantranmei/nodejs-api-starter/6d0184a784a9e67ce9d51a0b17dcad99540f2f5a/src/apis/routes/v2/.gitkeep -------------------------------------------------------------------------------- /src/apis/services/auth.service.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require('http-status') 2 | 3 | const tokenService = require('./token.service') 4 | const userService = require('./user.service') 5 | 6 | const ApiError = require('../../utils/api-error') 7 | 8 | /** 9 | * Login with username and password 10 | * @param {string} email 11 | * @param {string} password 12 | * @returns {Promise} 13 | */ 14 | const loginUserWithEmailAndPassword = async (email, password) => { 15 | const user = await userService.getUserByEmail(email) 16 | if (!user || !(await user.isPasswordMatch(password))) { 17 | throw new ApiError(httpStatus.UNAUTHORIZED, 'Incorrect email or password') 18 | } 19 | return user 20 | } 21 | 22 | /** 23 | * Logout 24 | * @param {string} refreshToken 25 | * @returns {Promise} 26 | */ 27 | const logout = async (refreshToken) => { 28 | const refreshTokenDoc = await tokenService.getTokenByRefresh(refreshToken, false) 29 | if (!refreshTokenDoc) { 30 | throw new ApiError(httpStatus.NOT_FOUND, 'Not found') 31 | } 32 | await refreshTokenDoc.remove() 33 | return true 34 | } 35 | 36 | module.exports = { 37 | loginUserWithEmailAndPassword, 38 | logout, 39 | } 40 | -------------------------------------------------------------------------------- /src/apis/services/index.js: -------------------------------------------------------------------------------- 1 | module.exports.authService = require('./auth.service') 2 | module.exports.taskService = require('./task.service') 3 | module.exports.tokenService = require('./token.service') 4 | module.exports.userService = require('./user.service') 5 | -------------------------------------------------------------------------------- /src/apis/services/task.service.js: -------------------------------------------------------------------------------- 1 | const { Task } = require('../models') 2 | 3 | /** 4 | * Create a task 5 | * @param {Object} taskBody 6 | * @returns {Promise} 7 | */ 8 | const createTask = async (taskBody) => { 9 | return Task.create(taskBody) 10 | } 11 | 12 | const deleteTask = async (taskId) => { 13 | return Task.deleteOne({ _id: taskId }) 14 | } 15 | 16 | /** 17 | * Get a task 18 | * @param {Object} taskBody 19 | * @returns {Promise} 20 | */ 21 | const getTasks = async () => { 22 | return Task.find({}).sort({ dueDate: -1 }).lean() 23 | } 24 | 25 | const updateTask = async (id, taskBody) => { 26 | const task = await Task.findOne({ _id: id }) 27 | 28 | Object.keys(taskBody).forEach((key) => { 29 | task[key] = taskBody[key] 30 | }) 31 | 32 | console.log('taskkkk: ', task) 33 | 34 | await task.save() 35 | 36 | return task 37 | } 38 | 39 | module.exports = { 40 | createTask, 41 | deleteTask, 42 | getTasks, 43 | updateTask, 44 | } 45 | -------------------------------------------------------------------------------- /src/apis/services/token.service.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const moment = require('moment') 3 | 4 | const { Token } = require('../models') 5 | 6 | const env = require('../../configs/env') 7 | const { tokenTypes } = require('../../configs/tokens') 8 | 9 | /** 10 | * Generate auth tokens 11 | * @param {User} user 12 | * @returns {Promise} 13 | */ 14 | const generateAuthTokens = async (user) => { 15 | const accessTokenExpires = moment().add(env.passport.jwtAccessExpired / 60, 'minutes') 16 | const accessToken = generateToken(user.id, accessTokenExpires, tokenTypes.ACCESS) 17 | 18 | const refreshTokenExpires = moment().add(env.passport.jwtRefreshExpired / 60, 'minutes') 19 | const refreshToken = generateToken(user.id, refreshTokenExpires, tokenTypes.REFRESH) 20 | await saveToken(refreshToken, user.id, refreshTokenExpires, tokenTypes.REFRESH) 21 | 22 | return { 23 | access: { 24 | token: accessToken, 25 | expires: accessTokenExpires.toDate(), 26 | }, 27 | refresh: { 28 | token: refreshToken, 29 | expires: refreshTokenExpires.toDate(), 30 | }, 31 | } 32 | } 33 | 34 | /** 35 | * Generate token 36 | * @param {ObjectId} userId 37 | * @param {Moment} expires 38 | * @param {string} type 39 | * @param {string} [secret] 40 | * @returns {string} 41 | */ 42 | const generateToken = (userId, expires, type, secret = env.passport.jwtToken) => { 43 | const payload = { 44 | sub: userId, 45 | iat: moment().unix(), 46 | exp: expires.unix(), 47 | type, 48 | } 49 | return jwt.sign(payload, secret) 50 | } 51 | 52 | /** 53 | * Get a token by refresh token 54 | * @param {string} refreshToken 55 | * @param {boolean} isBlackListed 56 | * @returns {Promise} 57 | */ 58 | const getTokenByRefresh = async (refreshToken, isBlackListed) => { 59 | const refreshTokenDoc = await Token.findOne({ 60 | token: refreshToken, 61 | type: tokenTypes.REFRESH, 62 | blacklisted: isBlackListed, 63 | }) 64 | return refreshTokenDoc 65 | } 66 | 67 | /** 68 | * Save a token 69 | * @param {string} token 70 | * @param {ObjectId} userId 71 | * @param {Moment} expires 72 | * @param {string} type 73 | * @param {boolean} [blacklisted] 74 | * @returns {Promise} 75 | */ 76 | const saveToken = async (token, userId, expires, type, blacklisted = false) => { 77 | const tokenDoc = await Token.create({ 78 | token, 79 | user: userId, 80 | expires: expires.toDate(), 81 | type, 82 | blacklisted, 83 | }) 84 | return tokenDoc 85 | } 86 | 87 | module.exports = { 88 | generateAuthTokens, 89 | generateToken, 90 | getTokenByRefresh, 91 | } 92 | -------------------------------------------------------------------------------- /src/apis/services/user.service.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require('http-status') 2 | 3 | const ApiError = require('../../utils/api-error') 4 | const { User } = require('../models') 5 | 6 | /** 7 | * Create a user 8 | * @param {Object} userBody 9 | * @returns {Promise} 10 | */ 11 | const createUser = async (userBody) => { 12 | if (await User.isEmailTaken(userBody.email)) { 13 | throw new ApiError(httpStatus.BAD_REQUEST, 'Email already taken') 14 | } 15 | return User.create(userBody) 16 | } 17 | 18 | /** 19 | * Get user by email 20 | * @param {string} email 21 | * @returns {Promise} 22 | */ 23 | const getUserByEmail = async (email) => { 24 | return User.findOne({ email }) 25 | } 26 | 27 | module.exports = { 28 | createUser, 29 | getUserByEmail, 30 | } 31 | -------------------------------------------------------------------------------- /src/apis/validations/auth.validation.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi') 2 | 3 | const { password } = require('./customize.validation') 4 | 5 | const loginSchema = { 6 | body: Joi.object().keys({ 7 | email: Joi.string().required(), 8 | password: Joi.string().required(), 9 | }), 10 | } 11 | 12 | const logoutSchema = { 13 | body: Joi.object().keys({ 14 | accessToken: Joi.string().required(), 15 | refreshToken: Joi.string().required(), 16 | }), 17 | } 18 | 19 | const registerSchema = { 20 | body: Joi.object().keys({ 21 | displayName: Joi.string().required(), 22 | email: Joi.string().required().email(), 23 | password: Joi.string().required().custom(password), 24 | }), 25 | } 26 | 27 | module.exports = { 28 | loginSchema, 29 | logoutSchema, 30 | registerSchema, 31 | } 32 | -------------------------------------------------------------------------------- /src/apis/validations/customize.validation.js: -------------------------------------------------------------------------------- 1 | const objectId = (value, helpers) => { 2 | if (!value.match(/^[0-9a-fA-F]{24}$/)) { 3 | return helpers.message('"{{#label}}" must be a valid id format') 4 | } 5 | return value 6 | } 7 | 8 | const password = (value, helpers) => { 9 | if (value.length < 6) { 10 | return helpers.message('password must be at least 6 characters') 11 | } 12 | if (!value.match(/\d/) || !value.match(/[a-zA-Z]/)) { 13 | return helpers.message('password must contain at least 1 letter and 1 number') 14 | } 15 | return value 16 | } 17 | 18 | module.exports = { 19 | objectId, 20 | password, 21 | } 22 | -------------------------------------------------------------------------------- /src/apis/validations/index.js: -------------------------------------------------------------------------------- 1 | module.exports.authValidation = require('./auth.validation') 2 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const Logger = require('./libs/logger') 2 | 3 | const bannerLogger = require('./libs/banner') 4 | 5 | const expressLoader = require('./loaders/expressLoader') 6 | const mongooseLoader = require('./loaders/mongooseLoader') 7 | const monitorLoader = require('./loaders/monitorLoader') 8 | const passportLoader = require('./loaders/passportLoader') 9 | const publicLoader = require('./loaders/publicLoader') 10 | const swaggerLoader = require('./loaders/swaggerLoader') 11 | const winstonLoader = require('./loaders/winstonLoader') 12 | 13 | /** 14 | * NODEJS API BOILERPLATE 15 | * ---------------------------------------- 16 | * 17 | * This is a boilerplate for Node.js Application written in Vanilla Javascript. 18 | * The basic layer of this app is express. For further information visit 19 | * the 'README.md' file. 20 | */ 21 | const log = new Logger(__filename) 22 | 23 | // Init loaders 24 | async function initApp() { 25 | // logging 26 | winstonLoader() 27 | 28 | // Database 29 | await mongooseLoader() 30 | 31 | // express 32 | const app = expressLoader() 33 | 34 | // monitor 35 | monitorLoader(app) 36 | 37 | // swagger 38 | swaggerLoader(app) 39 | 40 | // passport init 41 | passportLoader(app) 42 | 43 | // public Url 44 | publicLoader(app) 45 | } 46 | 47 | initApp() 48 | .then(() => bannerLogger(log)) 49 | .catch((error) => log.error('Application is crashed: ' + error)) 50 | -------------------------------------------------------------------------------- /src/configs/env.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | const path = require('path') 3 | 4 | const pkg = require('../../package.json') 5 | const { getOsEnvOptional, getOsEnv, normalizePort, toBool, toNumber } = require('../libs/os') 6 | 7 | /** 8 | * Load .env file or for tests the .env.test, .env.production file. 9 | */ 10 | dotenv.config({ path: path.join(process.cwd(), `.env${process.env.NODE_ENV === 'test' ? '.test' : ''}`) }) 11 | 12 | /** 13 | * Environment variables 14 | */ 15 | 16 | const env = { 17 | node: process.env.NODE_ENV || 'development', 18 | isProduction: process.env.NODE_ENV === 'production', 19 | isTest: process.env.NODE_ENV === 'test', 20 | isDevelopment: process.env.NODE_ENV === 'development', 21 | app: { 22 | name: getOsEnv('APP_NAME'), 23 | version: pkg.version, 24 | description: pkg.description, 25 | host: getOsEnv('APP_HOST'), 26 | schema: getOsEnv('APP_SCHEMA'), 27 | routePrefix: getOsEnv('APP_ROUTE_PREFIX'), 28 | port: normalizePort(process.env.PORT || getOsEnv('APP_PORT')), 29 | banner: toBool(getOsEnv('APP_BANNER')), 30 | }, 31 | database: { 32 | connection: getOsEnv('DB_CONNECTION'), 33 | }, 34 | log: { 35 | level: getOsEnv('LOG_LEVEL'), 36 | json: toBool(getOsEnvOptional('LOG_JSON')), 37 | output: getOsEnv('LOG_OUTPUT'), 38 | }, 39 | monitor: { 40 | enabled: toBool(getOsEnv('MONITOR_ENABLED')), 41 | route: getOsEnv('MONITOR_ROUTE'), 42 | username: getOsEnv('MONITOR_USERNAME'), 43 | password: getOsEnv('MONITOR_PASSWORD'), 44 | }, 45 | passport: { 46 | jwtToken: getOsEnv('PASSPORT_JWT'), 47 | jwtAccessExpired: toNumber(getOsEnv('PASSPORT_JWT_ACCESS_EXPIRED')), 48 | jwtRefreshExpired: toNumber(getOsEnv('PASSPORT_JWT_REFRESH_EXPIRED')), 49 | }, 50 | swagger: { 51 | enabled: toBool(getOsEnv('SWAGGER_ENABLED')), 52 | route: getOsEnv('SWAGGER_ROUTE'), 53 | username: getOsEnv('SWAGGER_USERNAME'), 54 | password: getOsEnv('SWAGGER_PASSWORD'), 55 | }, 56 | } 57 | 58 | module.exports = env 59 | -------------------------------------------------------------------------------- /src/configs/tokens.js: -------------------------------------------------------------------------------- 1 | const tokenTypes = { 2 | ACCESS: 'access', 3 | REFRESH: 'refresh', 4 | RESET_PASSWORD: 'resetPassword', 5 | VERIFY_EMAIL: 'verifyEmail', 6 | } 7 | 8 | module.exports = { 9 | tokenTypes, 10 | } 11 | -------------------------------------------------------------------------------- /src/docs/components.yml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | User: 4 | type: object 5 | properties: 6 | id: 7 | type: string 8 | email: 9 | type: string 10 | format: email 11 | name: 12 | type: string 13 | role: 14 | type: string 15 | enum: [user, admin] 16 | example: 17 | id: 5ebac534954b54139806c112 18 | email: fake@example.com 19 | name: fake name 20 | role: user 21 | 22 | Token: 23 | type: object 24 | properties: 25 | token: 26 | type: string 27 | expires: 28 | type: string 29 | format: date-time 30 | example: 31 | token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg 32 | expires: 2020-05-12T16:18:04.793Z 33 | 34 | AuthTokens: 35 | type: object 36 | properties: 37 | access: 38 | $ref: '#/components/schemas/Token' 39 | refresh: 40 | $ref: '#/components/schemas/Token' 41 | 42 | Error: 43 | type: object 44 | properties: 45 | code: 46 | type: number 47 | message: 48 | type: string 49 | 50 | responses: 51 | DuplicateEmail: 52 | description: Email already taken 53 | content: 54 | application/json: 55 | schema: 56 | $ref: '#/components/schemas/Error' 57 | example: 58 | code: 400 59 | message: Email already taken 60 | Unauthorized: 61 | description: Unauthorized 62 | content: 63 | application/json: 64 | schema: 65 | $ref: '#/components/schemas/Error' 66 | example: 67 | code: 401 68 | message: Please authenticate 69 | Forbidden: 70 | description: Forbidden 71 | content: 72 | application/json: 73 | schema: 74 | $ref: '#/components/schemas/Error' 75 | example: 76 | code: 403 77 | message: Forbidden 78 | NotFound: 79 | description: Not found 80 | content: 81 | application/json: 82 | schema: 83 | $ref: '#/components/schemas/Error' 84 | example: 85 | code: 404 86 | message: Not found 87 | 88 | securitySchemes: 89 | bearerAuth: 90 | type: http 91 | scheme: bearer 92 | bearerFormat: JWT 93 | -------------------------------------------------------------------------------- /src/libs/banner/index.js: -------------------------------------------------------------------------------- 1 | const env = require('../../configs/env') 2 | 3 | module.exports = (log) => { 4 | if (env.app.banner) { 5 | const route = () => `${env.app.schema}://${env.app.host}:${env.app.port}` 6 | log.info(``) 7 | log.info(`Sheeh, your app is ready on ${route()}${env.app.routePrefix}`) 8 | log.info(`To shut it down, press + C at any time.`) 9 | log.info(``) 10 | log.info('-------------------------------------------------------') 11 | log.info(`Environment : ${env.node}`) 12 | log.info(`Version : ${env.app.version}`) 13 | log.info(``) 14 | log.info(`API Info : ${route()}${env.app.routePrefix}`) 15 | if (env.swagger.enabled) { 16 | log.info(`Swagger : ${route()}${env.swagger.route}`) 17 | } 18 | if (env.monitor.enabled) { 19 | log.info(`Monitor : ${route()}${env.monitor.route}`) 20 | } 21 | log.info('-------------------------------------------------------') 22 | log.info('') 23 | } else { 24 | log.info(`Application is up and running.`) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/libs/logger/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const winston = require('winston') 3 | 4 | module.exports = class Logger { 5 | static DEFAULT_SCOPE = 'app' 6 | #scope 7 | 8 | constructor(scope) { 9 | this.#scope = Logger.parsePathToScope(scope ? scope : Logger.DEFAULT_SCOPE) 10 | } 11 | 12 | static parsePathToScope(filepath) { 13 | if (filepath.indexOf(path.sep) >= 0) { 14 | filepath = filepath.replace(process.cwd(), '') 15 | filepath = filepath.replace(`${path.sep}src${path.sep}`, '') 16 | filepath = filepath.replace(`${path.sep}dist${path.sep}`, '') 17 | filepath = filepath.replace('.ts', '') 18 | filepath = filepath.replace('.js', '') 19 | filepath = filepath.replace(path.sep, ':') 20 | } 21 | return filepath 22 | } 23 | 24 | _formatScope() { 25 | return `[${this.#scope}]` 26 | } 27 | 28 | _log(level, message, args) { 29 | if (winston) winston[level](`${this._formatScope()} ${message}`, args) 30 | } 31 | 32 | debug(message, ...args) { 33 | this._log('debug', message, args) 34 | } 35 | 36 | info(message, ...args) { 37 | this._log('info', message, args) 38 | } 39 | 40 | warn(message, ...args) { 41 | this._log('warn', message, args) 42 | } 43 | 44 | error(message, ...args) { 45 | this._log('error', message, args) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/libs/os.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | 3 | function getOsEnv(key) { 4 | if (typeof process.env[key] === 'undefined') { 5 | throw new Error(`Environment variable ${key} is not set.`) 6 | } 7 | 8 | return process.env[key] 9 | } 10 | 11 | function getOsEnvOptional(key) { 12 | return process.env[key] 13 | } 14 | 15 | function getPath(path) { 16 | return process.env.NODE_ENV === 'production' 17 | ? join(process.cwd(), path.replace('src/', 'dist/').slice(0, -3) + '.js') 18 | : join(process.cwd(), path) 19 | } 20 | 21 | function getPaths(paths) { 22 | return paths.map((p) => getPath(p)) 23 | } 24 | 25 | function getOsPath(key) { 26 | return getPath(getOsEnv(key)) 27 | } 28 | 29 | function getOsPaths(key) { 30 | return getPaths(getOsEnvArray(key)) 31 | } 32 | 33 | function getOsEnvArray(key, delimiter = ',') { 34 | return (process.env[key] && process.env[key].split(delimiter)) || [] 35 | } 36 | 37 | function toNumber(value) { 38 | return parseInt(value, 10) 39 | } 40 | 41 | function toBool(value) { 42 | return value === 'true' 43 | } 44 | 45 | function normalizePort(port) { 46 | const parsedPort = parseInt(port, 10) 47 | if (isNaN(parsedPort)) { 48 | // named pipe 49 | return port 50 | } 51 | if (parsedPort >= 0) { 52 | // port number 53 | return parsedPort 54 | } 55 | return false 56 | } 57 | 58 | module.exports = { 59 | getOsEnv, 60 | getOsEnvOptional, 61 | getPath, 62 | getPaths, 63 | getOsPath, 64 | getOsPaths, 65 | getOsEnvArray, 66 | toNumber, 67 | toBool, 68 | normalizePort, 69 | } 70 | -------------------------------------------------------------------------------- /src/loaders/expressLoader.js: -------------------------------------------------------------------------------- 1 | const compression = require('compression') 2 | const cors = require('cors') 3 | const express = require('express') 4 | const helmet = require('helmet') 5 | const mongoSanitize = require('express-mongo-sanitize') 6 | const morgan = require('morgan') 7 | const xss = require('xss-clean') 8 | 9 | const env = require('../configs/env') 10 | const { errorConverter, errorHandler } = require('../middlewares/error') 11 | const { customizeLimiter } = require('../middlewares/rate-limit') 12 | const routeConfig = require('../apis/routes') 13 | 14 | module.exports = () => { 15 | const app = express() 16 | 17 | // set log request 18 | app.use(morgan('dev')) 19 | 20 | // set security HTTP headers 21 | app.use(helmet()) 22 | 23 | // parse json request body 24 | app.use(express.json()) 25 | 26 | // parse urlencoded request body 27 | app.use(express.urlencoded({ extended: true })) 28 | 29 | // sanitize request data 30 | app.use(xss()) 31 | app.use(mongoSanitize()) 32 | 33 | // gzip compression 34 | app.use(compression()) 35 | 36 | // set cors blocked resources 37 | app.use(cors()) 38 | app.options('*', cors()) 39 | 40 | // setup limits 41 | if (env.isProduction) { 42 | // app.use('/v1/auth', customizeLimiter) 43 | } 44 | 45 | // api routes 46 | app.use(env.app.routePrefix, routeConfig) 47 | 48 | // convert error to ApiError, if needed 49 | app.use(errorConverter) 50 | 51 | // handle error 52 | app.use(errorHandler) 53 | 54 | app.listen(env.app.port) 55 | 56 | return app 57 | } 58 | -------------------------------------------------------------------------------- /src/loaders/mongooseLoader.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const env = require('../configs/env') 4 | const Logger = require('../libs/logger') 5 | 6 | const log = new Logger(__filename) 7 | 8 | module.exports = async () => { 9 | try { 10 | await mongoose.connect(env.database.connection, { 11 | useNewUrlParser: true, 12 | useUnifiedTopology: true, 13 | }) 14 | 15 | log.info('Successfully for MongoDB connected!!') 16 | } catch (err) { 17 | log.error(`Failed to connect to MongoDB - ${err.message}`) 18 | throw new Error(`Failed to connect to MongoDB`) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/loaders/monitorLoader.js: -------------------------------------------------------------------------------- 1 | const basicAuth = require('express-basic-auth') 2 | const monitor = require('express-status-monitor') 3 | 4 | const env = require('../configs/env') 5 | 6 | module.exports = (app) => { 7 | app.use(monitor()) 8 | app.get( 9 | env.monitor.route, 10 | env.monitor.username 11 | ? basicAuth({ 12 | users: { 13 | [`${env.monitor.username}`]: env.monitor.password, 14 | }, 15 | challenge: true, 16 | }) 17 | : (req, res, next) => next(), 18 | monitor().pageRoute 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/loaders/passportLoader.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport') 2 | 3 | const { jwtStrategy } = require('../apis/plugins/passport') 4 | 5 | module.exports = (app) => { 6 | // jwt authentication 7 | app.use(passport.initialize()) 8 | passport.use('jwt', jwtStrategy) 9 | } 10 | -------------------------------------------------------------------------------- /src/loaders/publicLoader.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const path = require('path') 3 | const favicon = require('serve-favicon') 4 | 5 | module.exports = (app) => { 6 | // Serve static files like images from the public folder 7 | app.use(express.static(path.join(__dirname, '..', 'public'), { maxAge: 31557600000 })) 8 | 9 | // A favicon is a visual cue that client software, like browsers, use to identify a site 10 | app.use(favicon(path.join(__dirname, '..', 'public', 'favicon.ico'))) 11 | } 12 | -------------------------------------------------------------------------------- /src/loaders/swaggerLoader.js: -------------------------------------------------------------------------------- 1 | const basicAuth = require('express-basic-auth') 2 | const swagger = require('swagger-ui-express') 3 | const swaggerJsDoc = require('swagger-jsdoc') 4 | 5 | const env = require('../configs/env') 6 | 7 | module.exports = (app) => { 8 | const swaggerOptions = { 9 | swaggerDefinition: { 10 | openapi: '3.0.0', 11 | info: { 12 | title: env.app.name, 13 | description: env.app.description, 14 | version: env.app.version, 15 | contact: { 16 | name: 'Tran Toan', 17 | email: 'trantoan.fox.97@gmail.com', 18 | }, 19 | license: { 20 | name: 'By © Tran Toan', 21 | url: 'https://www.facebook.com/trantoan960', 22 | }, 23 | }, 24 | servers: [ 25 | { 26 | url: `${env.app.schema}://${env.app.host}:${env.app.port}${env.app.routePrefix}`, 27 | }, 28 | ], 29 | }, 30 | apis: ['src/docs/*.yml', 'src/apis/routes/v1/*.js'], 31 | } 32 | 33 | if (env.isDevelopment) { 34 | app.use( 35 | env.swagger.route, 36 | env.swagger.username 37 | ? basicAuth({ 38 | users: { 39 | [`${env.swagger.username}`]: env.swagger.password, 40 | }, 41 | challenge: true, 42 | }) 43 | : (req, res, next) => next(), 44 | swagger.serve, 45 | swagger.setup(swaggerJsDoc(swaggerOptions)) 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/loaders/winstonLoader.js: -------------------------------------------------------------------------------- 1 | const { configure, format, transports } = require('winston') 2 | 3 | const env = require('../configs/env') 4 | 5 | module.exports = () => { 6 | configure({ 7 | transports: [ 8 | new transports.Console({ 9 | level: env.log.level, 10 | handleExceptions: true, 11 | format: 12 | env.node !== 'development' 13 | ? format.combine(format.json()) 14 | : format.combine(format.colorize(), format.simple()), 15 | }), 16 | ], 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/middlewares/error.js: -------------------------------------------------------------------------------- 1 | const httpStatus = require('http-status') 2 | const mongoose = require('mongoose') 3 | 4 | const ApiError = require('../utils/api-error') 5 | const env = require('../configs/env') 6 | const Logger = require('../libs/logger') 7 | 8 | const logger = new Logger(__filename) 9 | 10 | const errorConverter = (err, req, res, next) => { 11 | let error = err 12 | 13 | if (!(error instanceof ApiError)) { 14 | const statusCode = 15 | error.statusCode || error instanceof mongoose.Error ? httpStatus.BAD_REQUEST : httpStatus.INTERNAL_SERVER_ERROR 16 | const message = error.message || httpStatus[statusCode] 17 | error = new ApiError(statusCode, message, false, err.stack) 18 | } 19 | next(error) 20 | } 21 | 22 | const errorHandler = (err, req, res, next) => { 23 | const { statusCode, message } = err 24 | 25 | if (env.isProduction && !err.isOperational) { 26 | statusCode = httpStatus.INTERNAL_SERVER_ERROR 27 | message = httpStatus[httpStatus.INTERNAL_SERVER_ERROR] 28 | } 29 | 30 | const response = { 31 | code: statusCode, 32 | message, 33 | ...(env.isDevelopment && { stack: err.stack }), 34 | } 35 | 36 | if (env.isDevelopment) logger.error(err) 37 | 38 | return res.status(statusCode).json(response) 39 | } 40 | 41 | module.exports = { 42 | errorConverter, 43 | errorHandler, 44 | } 45 | -------------------------------------------------------------------------------- /src/middlewares/rate-limit.js: -------------------------------------------------------------------------------- 1 | const rateLimit = require('express-rate-limit') 2 | 3 | const customizeLimiter = rateLimit({ 4 | windowMs: 15 * 60 * 1000, 5 | max: 20, 6 | skipSuccessfulRequests: true, 7 | }) 8 | 9 | module.exports = { 10 | customizeLimiter, 11 | } 12 | -------------------------------------------------------------------------------- /src/middlewares/validate.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi') 2 | const httpStatus = require('http-status') 3 | 4 | const ApiError = require('../utils/api-error') 5 | const pickKeys = require('../utils/pick-keys') 6 | 7 | module.exports = (schema) => (req, res, next) => { 8 | const validSchema = pickKeys(schema, ['params', 'query', 'body']) 9 | const object = pickKeys(req, Object.keys(validSchema)) 10 | const { value, error } = Joi.compile(validSchema) 11 | .prefs({ errors: { label: 'key' }, abortEarly: false }) 12 | .validate(object) 13 | 14 | if (error) { 15 | const errorMessage = error.details.map((details) => details.message).join(', ') 16 | return next(new ApiError(httpStatus.BAD_REQUEST, errorMessage)) 17 | } 18 | Object.assign(req, value) 19 | return next() 20 | } 21 | -------------------------------------------------------------------------------- /src/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toantranmei/nodejs-api-starter/6d0184a784a9e67ce9d51a0b17dcad99540f2f5a/src/public/.gitkeep -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toantranmei/nodejs-api-starter/6d0184a784a9e67ce9d51a0b17dcad99540f2f5a/src/public/favicon.ico -------------------------------------------------------------------------------- /src/public/vendors/socket.io.min.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.io=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0){this.extraHeaders=opts.extraHeaders}}this.open()}Socket.priorWebsocketSuccess=false;Emitter(Socket.prototype);Socket.protocol=parser.protocol;Socket.Socket=Socket;Socket.Transport=_dereq_("./transport");Socket.transports=_dereq_("./transports");Socket.parser=_dereq_("engine.io-parser");Socket.prototype.createTransport=function(name){debug('creating transport "%s"',name);var query=clone(this.query);query.EIO=parser.protocol;query.transport=name;if(this.id)query.sid=this.id;var transport=new transports[name]({agent:this.agent,hostname:this.hostname,port:this.port,secure:this.secure,path:this.path,query:query,forceJSONP:this.forceJSONP,jsonp:this.jsonp,forceBase64:this.forceBase64,enablesXDR:this.enablesXDR,timestampRequests:this.timestampRequests,timestampParam:this.timestampParam,policyPort:this.policyPort,socket:this,pfx:this.pfx,key:this.key,passphrase:this.passphrase,cert:this.cert,ca:this.ca,ciphers:this.ciphers,rejectUnauthorized:this.rejectUnauthorized,perMessageDeflate:this.perMessageDeflate,extraHeaders:this.extraHeaders});return transport};function clone(obj){var o={};for(var i in obj){if(obj.hasOwnProperty(i)){o[i]=obj[i]}}return o}Socket.prototype.open=function(){var transport;if(this.rememberUpgrade&&Socket.priorWebsocketSuccess&&this.transports.indexOf("websocket")!=-1){transport="websocket"}else if(0===this.transports.length){var self=this;setTimeout(function(){self.emit("error","No transports available")},0);return}else{transport=this.transports[0]}this.readyState="opening";try{transport=this.createTransport(transport)}catch(e){this.transports.shift();this.open();return}transport.open();this.setTransport(transport)};Socket.prototype.setTransport=function(transport){debug("setting transport %s",transport.name);var self=this;if(this.transport){debug("clearing existing transport %s",this.transport.name);this.transport.removeAllListeners()}this.transport=transport;transport.on("drain",function(){self.onDrain()}).on("packet",function(packet){self.onPacket(packet)}).on("error",function(e){self.onError(e)}).on("close",function(){self.onClose("transport close")})};Socket.prototype.probe=function(name){debug('probing transport "%s"',name);var transport=this.createTransport(name,{probe:1}),failed=false,self=this;Socket.priorWebsocketSuccess=false;function onTransportOpen(){if(self.onlyBinaryUpgrades){var upgradeLosesBinary=!this.supportsBinary&&self.transport.supportsBinary;failed=failed||upgradeLosesBinary}if(failed)return;debug('probe transport "%s" opened',name);transport.send([{type:"ping",data:"probe"}]);transport.once("packet",function(msg){if(failed)return;if("pong"==msg.type&&"probe"==msg.data){debug('probe transport "%s" pong',name);self.upgrading=true;self.emit("upgrading",transport);if(!transport)return;Socket.priorWebsocketSuccess="websocket"==transport.name;debug('pausing current transport "%s"',self.transport.name);self.transport.pause(function(){if(failed)return;if("closed"==self.readyState)return;debug("changing transport and sending upgrade packet");cleanup();self.setTransport(transport);transport.send([{type:"upgrade"}]);self.emit("upgrade",transport);transport=null;self.upgrading=false;self.flush()})}else{debug('probe transport "%s" failed',name);var err=new Error("probe error");err.transport=transport.name;self.emit("upgradeError",err)}})}function freezeTransport(){if(failed)return;failed=true;cleanup();transport.close();transport=null}function onerror(err){var error=new Error("probe error: "+err);error.transport=transport.name;freezeTransport();debug('probe transport "%s" failed because of error: %s',name,err);self.emit("upgradeError",error)}function onTransportClose(){onerror("transport closed")}function onclose(){onerror("socket closed")}function onupgrade(to){if(transport&&to.name!=transport.name){debug('"%s" works - aborting "%s"',to.name,transport.name);freezeTransport()}}function cleanup(){transport.removeListener("open",onTransportOpen);transport.removeListener("error",onerror);transport.removeListener("close",onTransportClose);self.removeListener("close",onclose);self.removeListener("upgrading",onupgrade)}transport.once("open",onTransportOpen);transport.once("error",onerror);transport.once("close",onTransportClose);this.once("close",onclose);this.once("upgrading",onupgrade);transport.open()};Socket.prototype.onOpen=function(){debug("socket open");this.readyState="open";Socket.priorWebsocketSuccess="websocket"==this.transport.name;this.emit("open");this.flush();if("open"==this.readyState&&this.upgrade&&this.transport.pause){debug("starting upgrade probes");for(var i=0,l=this.upgrades.length;i';iframe=document.createElement(html)}catch(e){iframe=document.createElement("iframe");iframe.name=self.iframeId;iframe.src="javascript:0"}iframe.id=self.iframeId;self.form.appendChild(iframe);self.iframe=iframe}initIframe();data=data.replace(rEscapedNewline,"\\\n");this.area.value=data.replace(rNewline,"\\n");try{this.form.submit()}catch(e){}if(this.iframe.attachEvent){this.iframe.onreadystatechange=function(){if(self.iframe.readyState=="complete"){complete()}}}else{this.iframe.onload=complete}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:typeof global!=="undefined"?global:{})},{"./polling":8,"component-inherit":16}],7:[function(_dereq_,module,exports){(function(global){var XMLHttpRequest=_dereq_("xmlhttprequest-ssl");var Polling=_dereq_("./polling");var Emitter=_dereq_("component-emitter");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:polling-xhr");module.exports=XHR;module.exports.Request=Request;function empty(){}function XHR(opts){Polling.call(this,opts);if(global.location){var isSSL="https:"==location.protocol;var port=location.port;if(!port){port=isSSL?443:80}this.xd=opts.hostname!=global.location.hostname||port!=opts.port;this.xs=opts.secure!=isSSL}else{this.extraHeaders=opts.extraHeaders}}inherit(XHR,Polling);XHR.prototype.supportsBinary=true;XHR.prototype.request=function(opts){opts=opts||{};opts.uri=this.uri();opts.xd=this.xd;opts.xs=this.xs;opts.agent=this.agent||false;opts.supportsBinary=this.supportsBinary;opts.enablesXDR=this.enablesXDR;opts.pfx=this.pfx;opts.key=this.key;opts.passphrase=this.passphrase;opts.cert=this.cert;opts.ca=this.ca;opts.ciphers=this.ciphers;opts.rejectUnauthorized=this.rejectUnauthorized;opts.extraHeaders=this.extraHeaders;return new Request(opts)};XHR.prototype.doWrite=function(data,fn){var isBinary=typeof data!=="string"&&data!==undefined;var req=this.request({method:"POST",data:data,isBinary:isBinary});var self=this;req.on("success",fn);req.on("error",function(err){self.onError("xhr post error",err)});this.sendXhr=req};XHR.prototype.doPoll=function(){debug("xhr poll");var req=this.request();var self=this;req.on("data",function(data){self.onData(data)});req.on("error",function(err){self.onError("xhr poll error",err)});this.pollXhr=req};function Request(opts){this.method=opts.method||"GET";this.uri=opts.uri;this.xd=!!opts.xd;this.xs=!!opts.xs;this.async=false!==opts.async;this.data=undefined!=opts.data?opts.data:null;this.agent=opts.agent;this.isBinary=opts.isBinary;this.supportsBinary=opts.supportsBinary;this.enablesXDR=opts.enablesXDR;this.pfx=opts.pfx;this.key=opts.key;this.passphrase=opts.passphrase;this.cert=opts.cert;this.ca=opts.ca;this.ciphers=opts.ciphers;this.rejectUnauthorized=opts.rejectUnauthorized;this.extraHeaders=opts.extraHeaders;this.create()}Emitter(Request.prototype);Request.prototype.create=function(){var opts={agent:this.agent,xdomain:this.xd,xscheme:this.xs,enablesXDR:this.enablesXDR};opts.pfx=this.pfx;opts.key=this.key;opts.passphrase=this.passphrase;opts.cert=this.cert;opts.ca=this.ca;opts.ciphers=this.ciphers;opts.rejectUnauthorized=this.rejectUnauthorized;var xhr=this.xhr=new XMLHttpRequest(opts);var self=this;try{debug("xhr open %s: %s",this.method,this.uri);xhr.open(this.method,this.uri,this.async);try{if(this.extraHeaders){xhr.setDisableHeaderCheck(true);for(var i in this.extraHeaders){if(this.extraHeaders.hasOwnProperty(i)){xhr.setRequestHeader(i,this.extraHeaders[i])}}}}catch(e){}if(this.supportsBinary){xhr.responseType="arraybuffer"}if("POST"==this.method){try{if(this.isBinary){xhr.setRequestHeader("Content-type","application/octet-stream")}else{xhr.setRequestHeader("Content-type","text/plain;charset=UTF-8")}}catch(e){}}if("withCredentials"in xhr){xhr.withCredentials=true}if(this.hasXDR()){xhr.onload=function(){self.onLoad()};xhr.onerror=function(){self.onError(xhr.responseText)}}else{xhr.onreadystatechange=function(){if(4!=xhr.readyState)return;if(200==xhr.status||1223==xhr.status){self.onLoad()}else{setTimeout(function(){self.onError(xhr.status)},0)}}}debug("xhr data %s",this.data);xhr.send(this.data)}catch(e){setTimeout(function(){self.onError(e)},0);return}if(global.document){this.index=Request.requestsCount++;Request.requests[this.index]=this}};Request.prototype.onSuccess=function(){this.emit("success");this.cleanup()};Request.prototype.onData=function(data){this.emit("data",data);this.onSuccess()};Request.prototype.onError=function(err){this.emit("error",err);this.cleanup(true)};Request.prototype.cleanup=function(fromError){if("undefined"==typeof this.xhr||null===this.xhr){return}if(this.hasXDR()){this.xhr.onload=this.xhr.onerror=empty}else{this.xhr.onreadystatechange=empty}if(fromError){try{this.xhr.abort()}catch(e){}}if(global.document){delete Request.requests[this.index]}this.xhr=null};Request.prototype.onLoad=function(){var data;try{var contentType;try{contentType=this.xhr.getResponseHeader("Content-Type").split(";")[0]}catch(e){}if(contentType==="application/octet-stream"){data=this.xhr.response}else{if(!this.supportsBinary){data=this.xhr.responseText}else{try{data=String.fromCharCode.apply(null,new Uint8Array(this.xhr.response))}catch(e){var ui8Arr=new Uint8Array(this.xhr.response);var dataArray=[];for(var idx=0,length=ui8Arr.length;idxbytes){end=bytes}if(start>=bytes||start>=end||bytes===0){return new ArrayBuffer(0)}var abv=new Uint8Array(arraybuffer);var result=new Uint8Array(end-start);for(var i=start,ii=0;i>2]; 2 | base64+=chars[(bytes[i]&3)<<4|bytes[i+1]>>4];base64+=chars[(bytes[i+1]&15)<<2|bytes[i+2]>>6];base64+=chars[bytes[i+2]&63]}if(len%3===2){base64=base64.substring(0,base64.length-1)+"="}else if(len%3===1){base64=base64.substring(0,base64.length-2)+"=="}return base64};exports.decode=function(base64){var bufferLength=base64.length*.75,len=base64.length,i,p=0,encoded1,encoded2,encoded3,encoded4;if(base64[base64.length-1]==="="){bufferLength--;if(base64[base64.length-2]==="="){bufferLength--}}var arraybuffer=new ArrayBuffer(bufferLength),bytes=new Uint8Array(arraybuffer);for(i=0;i>4;bytes[p++]=(encoded2&15)<<4|encoded3>>2;bytes[p++]=(encoded3&3)<<6|encoded4&63}return arraybuffer}})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")},{}],14:[function(_dereq_,module,exports){(function(global){var BlobBuilder=global.BlobBuilder||global.WebKitBlobBuilder||global.MSBlobBuilder||global.MozBlobBuilder;var blobSupported=function(){try{var a=new Blob(["hi"]);return a.size===2}catch(e){return false}}();var blobSupportsArrayBufferView=blobSupported&&function(){try{var b=new Blob([new Uint8Array([1,2])]);return b.size===2}catch(e){return false}}();var blobBuilderSupported=BlobBuilder&&BlobBuilder.prototype.append&&BlobBuilder.prototype.getBlob;function mapArrayBufferViews(ary){for(var i=0;i=31}exports.formatters.j=function(v){return JSON.stringify(v)};function formatArgs(){var args=arguments;var useColors=this.useColors;args[0]=(useColors?"%c":"")+this.namespace+(useColors?" %c":" ")+args[0]+(useColors?"%c ":" ")+"+"+exports.humanize(this.diff);if(!useColors)return args;var c="color: "+this.color;args=[args[0],c,"color: inherit"].concat(Array.prototype.slice.call(args,1));var index=0;var lastC=0;args[0].replace(/%[a-z%]/g,function(match){if("%%"===match)return;index++;if("%c"===match){lastC=index}});args.splice(lastC,0,c);return args}function log(){return"object"===typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function save(namespaces){try{if(null==namespaces){exports.storage.removeItem("debug")}else{exports.storage.debug=namespaces}}catch(e){}}function load(){var r;try{r=exports.storage.debug}catch(e){}return r}exports.enable(load());function localstorage(){try{return window.localStorage}catch(e){}}},{"./debug":18}],18:[function(_dereq_,module,exports){exports=module.exports=debug;exports.coerce=coerce;exports.disable=disable;exports.enable=enable;exports.enabled=enabled;exports.humanize=_dereq_("ms");exports.names=[];exports.skips=[];exports.formatters={};var prevColor=0;var prevTime;function selectColor(){return exports.colors[prevColor++%exports.colors.length]}function debug(namespace){function disabled(){}disabled.enabled=false;function enabled(){var self=enabled;var curr=+new Date;var ms=curr-(prevTime||curr);self.diff=ms;self.prev=prevTime;self.curr=curr;prevTime=curr;if(null==self.useColors)self.useColors=exports.useColors();if(null==self.color&&self.useColors)self.color=selectColor();var args=Array.prototype.slice.call(arguments);args[0]=exports.coerce(args[0]);if("string"!==typeof args[0]){args=["%o"].concat(args)}var index=0;args[0]=args[0].replace(/%([a-z%])/g,function(match,format){if(match==="%%")return match;index++;var formatter=exports.formatters[format];if("function"===typeof formatter){var val=args[index];match=formatter.call(self,val);args.splice(index,1);index--}return match});if("function"===typeof exports.formatArgs){args=exports.formatArgs.apply(self,args)}var logFn=enabled.log||exports.log||console.log.bind(console);logFn.apply(self,args)}enabled.enabled=true;var fn=exports.enabled(namespace)?enabled:disabled;fn.namespace=namespace;return fn}function enable(namespaces){exports.save(namespaces);var split=(namespaces||"").split(/[\s,]+/);var len=split.length;for(var i=0;i1){return{type:packetslist[type],data:data.substring(1)}}else{return{type:packetslist[type]}}}var asArray=new Uint8Array(data);var type=asArray[0];var rest=sliceBuffer(data,1);if(Blob&&binaryType==="blob"){rest=new Blob([rest])}return{type:packetslist[type],data:rest}};exports.decodeBase64Packet=function(msg,binaryType){var type=packetslist[msg.charAt(0)];if(!global.ArrayBuffer){return{type:type,data:{base64:true,data:msg.substr(1)}}}var data=base64encoder.decode(msg.substr(1));if(binaryType==="blob"&&Blob){data=new Blob([data])}return{type:type,data:data}};exports.encodePayload=function(packets,supportsBinary,callback){if(typeof supportsBinary=="function"){callback=supportsBinary;supportsBinary=null}var isBinary=hasBinary(packets);if(supportsBinary&&isBinary){if(Blob&&!dontSendBlobs){return exports.encodePayloadAsBlob(packets,callback)}return exports.encodePayloadAsArrayBuffer(packets,callback)}if(!packets.length){return callback("0:")}function setLengthHeader(message){return message.length+":"+message}function encodeOne(packet,doneCallback){exports.encodePacket(packet,!isBinary?false:supportsBinary,true,function(message){doneCallback(null,setLengthHeader(message))})}map(packets,encodeOne,function(err,results){return callback(results.join(""))})};function map(ary,each,done){var result=new Array(ary.length);var next=after(ary.length,done);var eachWithIndex=function(i,el,cb){each(el,function(error,msg){result[i]=msg;cb(error,result)})};for(var i=0;i0){var tailArray=new Uint8Array(bufferTail);var isString=tailArray[0]===0;var msgLength="";for(var i=1;;i++){if(tailArray[i]==255)break;if(msgLength.length>310){numberTooLong=true;break}msgLength+=tailArray[i]}if(numberTooLong)return callback(err,0,1);bufferTail=sliceBuffer(bufferTail,2+msgLength.length);msgLength=parseInt(msgLength);var msg=sliceBuffer(bufferTail,0,msgLength);if(isString){try{msg=String.fromCharCode.apply(null,new Uint8Array(msg))}catch(e){var typed=new Uint8Array(msg);msg="";for(var i=0;i1e4)return;var match=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(str);if(!match)return;var n=parseFloat(match[1]);var type=(match[2]||"ms").toLowerCase();switch(type){case"years":case"year":case"yrs":case"yr":case"y":return n*y;case"days":case"day":case"d":return n*d;case"hours":case"hour":case"hrs":case"hr":case"h":return n*h;case"minutes":case"minute":case"mins":case"min":case"m":return n*m;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n}}function short(ms){if(ms>=d)return Math.round(ms/d)+"d";if(ms>=h)return Math.round(ms/h)+"h";if(ms>=m)return Math.round(ms/m)+"m";if(ms>=s)return Math.round(ms/s)+"s";return ms+"ms"}function long(ms){return plural(ms,d,"day")||plural(ms,h,"hour")||plural(ms,m,"minute")||plural(ms,s,"second")||ms+" ms"}function plural(ms,n,name){if(ms=55296&&value<=56319&&counter65535){value-=65536;output+=stringFromCharCode(value>>>10&1023|55296);value=56320|value&1023}output+=stringFromCharCode(value)}return output}function checkScalarValue(codePoint){if(codePoint>=55296&&codePoint<=57343){throw Error("Lone surrogate U+"+codePoint.toString(16).toUpperCase()+" is not a scalar value")}}function createByte(codePoint,shift){return stringFromCharCode(codePoint>>shift&63|128)}function encodeCodePoint(codePoint){if((codePoint&4294967168)==0){return stringFromCharCode(codePoint)}var symbol="";if((codePoint&4294965248)==0){symbol=stringFromCharCode(codePoint>>6&31|192)}else if((codePoint&4294901760)==0){checkScalarValue(codePoint);symbol=stringFromCharCode(codePoint>>12&15|224);symbol+=createByte(codePoint,6)}else if((codePoint&4292870144)==0){symbol=stringFromCharCode(codePoint>>18&7|240);symbol+=createByte(codePoint,12);symbol+=createByte(codePoint,6)}symbol+=stringFromCharCode(codePoint&63|128);return symbol}function utf8encode(string){var codePoints=ucs2decode(string);var length=codePoints.length;var index=-1;var codePoint;var byteString="";while(++index=byteCount){throw Error("Invalid byte index")}var continuationByte=byteArray[byteIndex]&255;byteIndex++;if((continuationByte&192)==128){return continuationByte&63}throw Error("Invalid continuation byte")}function decodeSymbol(){var byte1;var byte2;var byte3;var byte4;var codePoint;if(byteIndex>byteCount){throw Error("Invalid byte index")}if(byteIndex==byteCount){return false}byte1=byteArray[byteIndex]&255;byteIndex++;if((byte1&128)==0){return byte1}if((byte1&224)==192){var byte2=readContinuationByte();codePoint=(byte1&31)<<6|byte2;if(codePoint>=128){return codePoint}else{throw Error("Invalid continuation byte")}}if((byte1&240)==224){byte2=readContinuationByte();byte3=readContinuationByte();codePoint=(byte1&15)<<12|byte2<<6|byte3;if(codePoint>=2048){checkScalarValue(codePoint);return codePoint}else{throw Error("Invalid continuation byte")}}if((byte1&248)==240){byte2=readContinuationByte();byte3=readContinuationByte();byte4=readContinuationByte();codePoint=(byte1&15)<<18|byte2<<12|byte3<<6|byte4;if(codePoint>=65536&&codePoint<=1114111){return codePoint}}throw Error("Invalid UTF-8 detected")}var byteArray;var byteCount;var byteIndex;function utf8decode(byteString){byteArray=ucs2decode(byteString);byteCount=byteArray.length;byteIndex=0;var codePoints=[];var tmp;while((tmp=decodeSymbol())!==false){codePoints.push(tmp)}return ucs2encode(codePoints)}var utf8={version:"2.0.0",encode:utf8encode,decode:utf8decode};if(typeof define=="function"&&typeof define.amd=="object"&&define.amd){define(function(){return utf8})}else if(freeExports&&!freeExports.nodeType){if(freeModule){freeModule.exports=utf8}else{var object={};var hasOwnProperty=object.hasOwnProperty;for(var key in utf8){hasOwnProperty.call(utf8,key)&&(freeExports[key]=utf8[key])}}}else{root.utf8=utf8}})(this)}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:typeof global!=="undefined"?global:{})},{}],30:[function(_dereq_,module,exports){"use strict";var alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".split(""),length=64,map={},seed=0,i=0,prev;function encode(num){var encoded="";do{encoded=alphabet[num%length]+encoded;num=Math.floor(num/length)}while(num>0);return encoded}function decode(str){var decoded=0;for(i=0;i0&&!this.encoding){var pack=this.packetBuffer.shift();this.packet(pack)}};Manager.prototype.cleanup=function(){debug("cleanup");var sub;while(sub=this.subs.shift())sub.destroy();this.packetBuffer=[];this.encoding=false;this.lastPing=null;this.decoder.destroy()};Manager.prototype.close=Manager.prototype.disconnect=function(){debug("disconnect");this.skipReconnect=true;this.reconnecting=false;if("opening"==this.readyState){this.cleanup()}this.backoff.reset();this.readyState="closed";if(this.engine)this.engine.close()};Manager.prototype.onclose=function(reason){debug("onclose");this.cleanup();this.backoff.reset();this.readyState="closed";this.emit("close",reason);if(this._reconnection&&!this.skipReconnect){this.reconnect()}};Manager.prototype.reconnect=function(){if(this.reconnecting||this.skipReconnect)return this;var self=this;if(this.backoff.attempts>=this._reconnectionAttempts){debug("reconnect failed");this.backoff.reset();this.emitAll("reconnect_failed");this.reconnecting=false}else{var delay=this.backoff.duration();debug("will wait %dms before reconnect attempt",delay);this.reconnecting=true;var timer=setTimeout(function(){if(self.skipReconnect)return;debug("attempting reconnect");self.emitAll("reconnect_attempt",self.backoff.attempts);self.emitAll("reconnecting",self.backoff.attempts);if(self.skipReconnect)return;self.open(function(err){if(err){debug("reconnect attempt error");self.reconnecting=false;self.reconnect();self.emitAll("reconnect_error",err.data)}else{debug("reconnect success");self.onreconnect()}})},delay);this.subs.push({destroy:function(){clearTimeout(timer)}})}};Manager.prototype.onreconnect=function(){var attempt=this.backoff.attempts;this.reconnecting=false;this.backoff.reset();this.updateSocketIds();this.emitAll("reconnect",attempt)}},{"./on":33,"./socket":34,backo2:36,"component-bind":37,"component-emitter":38,debug:39,"engine.io-client":1,indexof:42,"socket.io-parser":47}],33:[function(_dereq_,module,exports){module.exports=on;function on(obj,ev,fn){obj.on(ev,fn);return{destroy:function(){obj.removeListener(ev,fn)}}}},{}],34:[function(_dereq_,module,exports){var parser=_dereq_("socket.io-parser");var Emitter=_dereq_("component-emitter");var toArray=_dereq_("to-array");var on=_dereq_("./on");var bind=_dereq_("component-bind");var debug=_dereq_("debug")("socket.io-client:socket");var hasBin=_dereq_("has-binary");module.exports=exports=Socket;var events={connect:1,connect_error:1,connect_timeout:1,connecting:1,disconnect:1,error:1,reconnect:1,reconnect_attempt:1,reconnect_failed:1,reconnect_error:1,reconnecting:1,ping:1,pong:1};var emit=Emitter.prototype.emit;function Socket(io,nsp){this.io=io;this.nsp=nsp;this.json=this;this.ids=0;this.acks={};this.receiveBuffer=[];this.sendBuffer=[];this.connected=false;this.disconnected=true;if(this.io.autoConnect)this.open()}Emitter(Socket.prototype);Socket.prototype.subEvents=function(){if(this.subs)return;var io=this.io;this.subs=[on(io,"open",bind(this,"onopen")),on(io,"packet",bind(this,"onpacket")),on(io,"close",bind(this,"onclose"))]};Socket.prototype.open=Socket.prototype.connect=function(){if(this.connected)return this;this.subEvents();this.io.open();if("open"==this.io.readyState)this.onopen();this.emit("connecting");return this};Socket.prototype.send=function(){var args=toArray(arguments);args.unshift("message");this.emit.apply(this,args);return this};Socket.prototype.emit=function(ev){if(events.hasOwnProperty(ev)){emit.apply(this,arguments);return this}var args=toArray(arguments);var parserType=parser.EVENT;if(hasBin(args)){parserType=parser.BINARY_EVENT}var packet={type:parserType,data:args};packet.options={};packet.options.compress=!this.flags||false!==this.flags.compress;if("function"==typeof args[args.length-1]){debug("emitting packet with ack id %d",this.ids);this.acks[this.ids]=args.pop();packet.id=this.ids++}if(this.connected){this.packet(packet)}else{this.sendBuffer.push(packet)}delete this.flags;return this};Socket.prototype.packet=function(packet){packet.nsp=this.nsp;this.io.packet(packet)};Socket.prototype.onopen=function(){debug("transport is open - connecting");if("/"!=this.nsp){this.packet({type:parser.CONNECT})}};Socket.prototype.onclose=function(reason){debug("close (%s)",reason);this.connected=false;this.disconnected=true;delete this.id;this.emit("disconnect",reason)};Socket.prototype.onpacket=function(packet){if(packet.nsp!=this.nsp)return;switch(packet.type){case parser.CONNECT:this.onconnect();break;case parser.EVENT:this.onevent(packet);break;case parser.BINARY_EVENT:this.onevent(packet);break;case parser.ACK:this.onack(packet);break;case parser.BINARY_ACK:this.onack(packet);break;case parser.DISCONNECT:this.ondisconnect();break;case parser.ERROR:this.emit("error",packet.data);break}};Socket.prototype.onevent=function(packet){var args=packet.data||[];debug("emitting event %j",args);if(null!=packet.id){debug("attaching ack callback to event");args.push(this.ack(packet.id))}if(this.connected){emit.apply(this,args)}else{this.receiveBuffer.push(args)}};Socket.prototype.ack=function(id){var self=this;var sent=false;return function(){if(sent)return;sent=true;var args=toArray(arguments);debug("sending ack %j",args);var type=hasBin(args)?parser.BINARY_ACK:parser.ACK;self.packet({type:type,id:id,data:args})}};Socket.prototype.onack=function(packet){var ack=this.acks[packet.id];if("function"==typeof ack){debug("calling ack %s with %j",packet.id,packet.data);ack.apply(this,packet.data);delete this.acks[packet.id]}else{debug("bad ack %s",packet.id)}};Socket.prototype.onconnect=function(){this.connected=true;this.disconnected=false;this.emit("connect");this.emitBuffered()};Socket.prototype.emitBuffered=function(){var i;for(i=0;i0&&opts.jitter<=1?opts.jitter:0;this.attempts=0}Backoff.prototype.duration=function(){var ms=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var rand=Math.random();var deviation=Math.floor(rand*this.jitter*ms);ms=(Math.floor(rand*10)&1)==0?ms-deviation:ms+deviation}return Math.min(ms,this.max)|0};Backoff.prototype.reset=function(){this.attempts=0};Backoff.prototype.setMin=function(min){this.ms=min};Backoff.prototype.setMax=function(max){this.max=max};Backoff.prototype.setJitter=function(jitter){this.jitter=jitter}},{}],37:[function(_dereq_,module,exports){var slice=[].slice;module.exports=function(obj,fn){if("string"==typeof fn)fn=obj[fn];if("function"!=typeof fn)throw new Error("bind() requires a function");var args=slice.call(arguments,2);return function(){return fn.apply(obj,args.concat(slice.call(arguments)))}}},{}],38:[function(_dereq_,module,exports){module.exports=Emitter;function Emitter(obj){if(obj)return mixin(obj)}function mixin(obj){for(var key in Emitter.prototype){obj[key]=Emitter.prototype[key]}return obj}Emitter.prototype.on=Emitter.prototype.addEventListener=function(event,fn){this._callbacks=this._callbacks||{};(this._callbacks["$"+event]=this._callbacks["$"+event]||[]).push(fn);return this};Emitter.prototype.once=function(event,fn){function on(){this.off(event,on);fn.apply(this,arguments)}on.fn=fn;this.on(event,on);return this};Emitter.prototype.off=Emitter.prototype.removeListener=Emitter.prototype.removeAllListeners=Emitter.prototype.removeEventListener=function(event,fn){this._callbacks=this._callbacks||{};if(0==arguments.length){this._callbacks={};return this}var callbacks=this._callbacks["$"+event];if(!callbacks)return this;if(1==arguments.length){delete this._callbacks["$"+event];return this}var cb;for(var i=0;i1)))/4)-floor((year-1901+month)/100)+floor((year-1601+month)/400)}}if(!(isProperty=objectProto.hasOwnProperty)){isProperty=function(property){var members={},constructor;if((members.__proto__=null,members.__proto__={toString:1},members).toString!=getClass){isProperty=function(property){var original=this.__proto__,result=property in(this.__proto__=null,this);this.__proto__=original;return result}}else{constructor=members.constructor;isProperty=function(property){var parent=(this.constructor||constructor).prototype;return property in this&&!(property in parent&&this[property]===parent[property])}}members=null;return isProperty.call(this,property)}}forEach=function(object,callback){var size=0,Properties,members,property;(Properties=function(){this.valueOf=0}).prototype.valueOf=0;members=new Properties;for(property in members){if(isProperty.call(members,property)){size++}}Properties=members=null;if(!size){members=["valueOf","toString","toLocaleString","propertyIsEnumerable","isPrototypeOf","hasOwnProperty","constructor"];forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,length;var hasProperty=!isFunction&&typeof object.constructor!="function"&&objectTypes[typeof object.hasOwnProperty]&&object.hasOwnProperty||isProperty;for(property in object){if(!(isFunction&&property=="prototype")&&hasProperty.call(object,property)){callback(property)}}for(length=members.length;property=members[--length];hasProperty.call(object,property)&&callback(property));}}else if(size==2){forEach=function(object,callback){var members={},isFunction=getClass.call(object)==functionClass,property;for(property in object){if(!(isFunction&&property=="prototype")&&!isProperty.call(members,property)&&(members[property]=1)&&isProperty.call(object,property)){callback(property)}}}}else{forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,isConstructor;for(property in object){if(!(isFunction&&property=="prototype")&&isProperty.call(object,property)&&!(isConstructor=property==="constructor")){callback(property)}}if(isConstructor||isProperty.call(object,property="constructor")){callback(property)}}}return forEach(object,callback)};if(!has("json-stringify")){var Escapes={92:"\\\\",34:'\\"',8:"\\b",12:"\\f",10:"\\n",13:"\\r",9:"\\t"};var leadingZeroes="000000";var toPaddedString=function(width,value){return(leadingZeroes+(value||0)).slice(-width)};var unicodePrefix="\\u00";var quote=function(value){var result='"',index=0,length=value.length,useCharIndex=!charIndexBuggy||length>10;var symbols=useCharIndex&&(charIndexBuggy?value.split(""):value);for(;index-1/0&&value<1/0){if(getDay){date=floor(value/864e5);for(year=floor(date/365.2425)+1970-1;getDay(year+1,0)<=date;year++);for(month=floor((date-getDay(year,0))/30.42);getDay(year,month+1)<=date;month++);date=1+date-getDay(year,month);time=(value%864e5+864e5)%864e5;hours=floor(time/36e5)%24;minutes=floor(time/6e4)%60;seconds=floor(time/1e3)%60;milliseconds=time%1e3}else{year=value.getUTCFullYear();month=value.getUTCMonth();date=value.getUTCDate();hours=value.getUTCHours();minutes=value.getUTCMinutes();seconds=value.getUTCSeconds();milliseconds=value.getUTCMilliseconds()}value=(year<=0||year>=1e4?(year<0?"-":"+")+toPaddedString(6,year<0?-year:year):toPaddedString(4,year))+"-"+toPaddedString(2,month+1)+"-"+toPaddedString(2,date)+"T"+toPaddedString(2,hours)+":"+toPaddedString(2,minutes)+":"+toPaddedString(2,seconds)+"."+toPaddedString(3,milliseconds)+"Z"}else{value=null}}else if(typeof value.toJSON=="function"&&(className!=numberClass&&className!=stringClass&&className!=arrayClass||isProperty.call(value,"toJSON"))){value=value.toJSON(property)}}if(callback){value=callback.call(object,property,value)}if(value===null){return"null"}className=getClass.call(value);if(className==booleanClass){return""+value}else if(className==numberClass){return value>-1/0&&value<1/0?""+value:"null"}else if(className==stringClass){return quote(""+value)}if(typeof value=="object"){for(length=stack.length;length--;){if(stack[length]===value){throw TypeError()}}stack.push(value);results=[];prefix=indentation;indentation+=whitespace;if(className==arrayClass){for(index=0,length=value.length;index0){for(whitespace="",width>10&&(width=10);whitespace.length=48&&charCode<=57||charCode>=97&&charCode<=102||charCode>=65&&charCode<=70)){abort()}}value+=fromCharCode("0x"+source.slice(begin,Index));break;default:abort()}}else{if(charCode==34){break}charCode=source.charCodeAt(Index);begin=Index;while(charCode>=32&&charCode!=92&&charCode!=34){charCode=source.charCodeAt(++Index)}value+=source.slice(begin,Index)}}if(source.charCodeAt(Index)==34){Index++;return value}abort();default:begin=Index;if(charCode==45){isSigned=true;charCode=source.charCodeAt(++Index)}if(charCode>=48&&charCode<=57){if(charCode==48&&(charCode=source.charCodeAt(Index+1),charCode>=48&&charCode<=57)){abort()}isSigned=false;for(;Index=48&&charCode<=57);Index++);if(source.charCodeAt(Index)==46){position=++Index;for(;position=48&&charCode<=57);position++);if(position==Index){abort()}Index=position}charCode=source.charCodeAt(Index);if(charCode==101||charCode==69){charCode=source.charCodeAt(++Index);if(charCode==43||charCode==45){Index++}for(position=Index;position=48&&charCode<=57);position++);if(position==Index){abort()}Index=position}return+source.slice(begin,Index)}if(isSigned){abort()}if(source.slice(Index,Index+4)=="true"){Index+=4;return true}else if(source.slice(Index,Index+5)=="false"){Index+=5;return false}else if(source.slice(Index,Index+4)=="null"){Index+=4;return null}abort()}}return"$"};var get=function(value){var results,hasMembers;if(value=="$"){abort()}if(typeof value=="string"){if((charIndexBuggy?value.charAt(0):value[0])=="@"){return value.slice(1)}if(value=="["){results=[];for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="]"){break}if(hasMembers){if(value==","){value=lex();if(value=="]"){abort()}}else{abort()}}if(value==","){abort()}results.push(get(value))}return results}else if(value=="{"){results={};for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="}"){break}if(hasMembers){if(value==","){value=lex();if(value=="}"){abort()}}else{abort()}}if(value==","||typeof value!="string"||(charIndexBuggy?value.charAt(0):value[0])!="@"||lex()!=":"){abort()}results[value.slice(1)]=get(lex())}return results 4 | }abort()}return value};var update=function(source,property,callback){var element=walk(source,property,callback);if(element===undef){delete source[property]}else{source[property]=element}};var walk=function(source,property,callback){var value=source[property],length;if(typeof value=="object"&&value){if(getClass.call(value)==arrayClass){for(length=value.length;length--;){update(value,length,callback)}}else{forEach(value,function(property){update(value,property,callback)})}}return callback.call(source,property,value)};exports.parse=function(source,callback){var result,value;Index=0;Source=""+source;result=get(lex());if(lex()!="$"){abort()}Index=Source=null;return callback&&getClass.call(callback)==functionClass?walk((value={},value[""]=result,value),"",callback):result}}}exports["runInContext"]=runInContext;return exports}if(freeExports&&!isLoader){runInContext(root,freeExports)}else{var nativeJSON=root.JSON,previousJSON=root["JSON3"],isRestored=false;var JSON3=runInContext(root,root["JSON3"]={noConflict:function(){if(!isRestored){isRestored=true;root.JSON=nativeJSON;root["JSON3"]=previousJSON;nativeJSON=previousJSON=null}return JSON3}});root.JSON={parse:JSON3.parse,stringify:JSON3.stringify}}if(isLoader){define(function(){return JSON3})}}).call(this)}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:typeof global!=="undefined"?global:{})},{}],51:[function(_dereq_,module,exports){module.exports=toArray;function toArray(list,index){var array=[];index=index||0;for(var i=index||0;i (req, res, next) => { 2 | Promise.resolve(fn(req, res, next)).catch((err) => next(err)) 3 | } 4 | 5 | module.exports = catchAsync 6 | -------------------------------------------------------------------------------- /src/utils/pick-keys.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create an object composed of the picked object properties 3 | * @param {Object} object 4 | * @param {string[]} keys 5 | * @returns {Object} 6 | */ 7 | module.exports = (object, keys) => { 8 | return keys.reduce((obj, key) => { 9 | if (object && Object.prototype.hasOwnProperty.call(object, key)) { 10 | obj[key] = object[key] 11 | } 12 | return obj 13 | }, {}) 14 | } 15 | --------------------------------------------------------------------------------