[
  {
    "path": ".dockerignore",
    "content": "node_modules\n.git\n.gitignore\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": ".env.example",
    "content": "# Port number\nPORT=3000\n\n# URL of the Mongo DB\nMONGODB_URL=mongodb://127.0.0.1:27017/node-boilerplate\n\n# JWT\n# JWT secret key\nJWT_SECRET=thisisasamplesecret\n# Number of minutes after which an access token expires\nJWT_ACCESS_EXPIRATION_MINUTES=30\n# Number of days after which a refresh token expires\nJWT_REFRESH_EXPIRATION_DAYS=30\n# Number of minutes after which a reset password token expires\nJWT_RESET_PASSWORD_EXPIRATION_MINUTES=10\n# Number of minutes after which a verify email token expires\nJWT_VERIFY_EMAIL_EXPIRATION_MINUTES=10\n\n# SMTP configuration options for the email service\n# For testing, you can use a fake SMTP service like Ethereal: https://ethereal.email/create\nSMTP_HOST=email-server\nSMTP_PORT=587\nSMTP_USERNAME=email-server-username\nSMTP_PASSWORD=email-server-password\nEMAIL_FROM=support@yourapp.com\n"
  },
  {
    "path": ".eslintignore",
    "content": "node_modules\nbin\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"env\": {\n    \"node\": true,\n    \"jest\": true\n  },\n  \"extends\": [\"airbnb-base\", \"plugin:jest/recommended\", \"plugin:security/recommended\", \"plugin:prettier/recommended\"],\n  \"plugins\": [\"jest\", \"security\", \"prettier\"],\n  \"parserOptions\": {\n    \"ecmaVersion\": 2018\n  },\n  \"rules\": {\n    \"no-console\": \"error\",\n    \"func-names\": \"off\",\n    \"no-underscore-dangle\": \"off\",\n    \"consistent-return\": \"off\",\n    \"jest/expect-expect\": \"off\",\n    \"security/detect-object-injection\": \"off\"\n  }\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Convert text file line endings to lf\n* text eol=lf\n*.js text\n"
  },
  {
    "path": ".gitignore",
    "content": "# Dependencies\nnode_modules\n\n# yarn error logs\nyarn-error.log\n\n# Environment varibales\n.env*\n!.env*.example\n\n# Code coverage\ncoverage\n"
  },
  {
    "path": ".husky/post-checkout",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nyarn install\n"
  },
  {
    "path": ".husky/post-commit",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\ngit status\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nyarn lint-staged\n"
  },
  {
    "path": ".lintstagedrc.json",
    "content": "{\n  \"*.js\": \"eslint\"\n}\n"
  },
  {
    "path": ".prettierignore",
    "content": "node_modules\ncoverage\n\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"singleQuote\": true,\n  \"printWidth\": 125\n}\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - '12'\nservices:\n  - mongodb\ncache: yarn\nbranches:\n  only:\n    - master\nenv:\n  global:\n    - PORT=3000\n    - MONGODB_URL=mongodb://localhost:27017/node-boilerplate\n    - JWT_SECRET=thisisasamplesecret\n    - JWT_ACCESS_EXPIRATION_MINUTES=30\n    - JWT_REFRESH_EXPIRATION_DAYS=30\nscript:\n  - yarn lint\n  - yarn test\nafter_success: yarn coverage:coveralls\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.\n\n## [1.7.0](https://github.com/hagopj13/node-express-boilerplate/compare/v1.6.0...v1.7.0) (2021-03-30)\n\n### Features\n\n- add email verification feature ([#78](https://github.com/hagopj13/node-express-boilerplate/pull/78)) ([9dae3f2](https://github.com/hagopj13/node-express-boilerplate/commit/9dae3f27df371103b6a9f96924980d2d8d7ba14e))\n\n## [1.6.0](https://github.com/hagopj13/node-express-boilerplate/compare/v1.5.0...v1.6.0) (2020-12-27)\n\n### Features\n\n- add script to create app using npm init ([acf6fdf](https://github.com/hagopj13/node-express-boilerplate/commit/acf6fdfd105bba476efb171f8cd92d752ecad691))\n- disable docs in production ([#59](https://github.com/hagopj13/node-express-boilerplate/pull/59)) ([68d1e33](https://github.com/hagopj13/node-express-boilerplate/commit/68d1e33194c46df93fc99d6e65ecf5feeecd354b))\n- add populate feature to the paginate plugin ([#45](https://github.com/hagopj13/node-express-boilerplate/pull/45)) ([9cf9535](https://github.com/hagopj13/node-express-boilerplate/commit/9cf953553556bc5060821dc630a2d2d5e12da37f))\n- add nested private fields feature to the toJSON plugin ([#47](https://github.com/hagopj13/node-express-boilerplate/pull/47)) ([5ba8628](https://github.com/hagopj13/node-express-boilerplate/commit/5ba8628ea18ffc90d39f0b8bb1241bebdb6cf675))\n\n## [1.5.0](https://github.com/hagopj13/node-express-boilerplate/compare/v1.4.1...v1.5.0) (2020-09-28)\n\n### Features\n\n- add sorting by multiple criteria option ([677ee12](https://github.com/hagopj13/node-express-boilerplate/commit/677ee12808ba1cf02e422498ae464159345dc76f)), closes [#29](https://github.com/hagopj13/node-express-boilerplate/issues/29)\n\n## [1.4.1](https://github.com/hagopj13/node-express-boilerplate/compare/v1.4.0...v1.4.1) (2020-09-14)\n\n### Bug Fixes\n\n- upgrade mongoose to solve vulnerability issue ([1650bdf](https://github.com/hagopj13/node-express-boilerplate/commit/1650bdf1bf36ce13597c0ed3503c7b4abef01ee5))\n- add type to token payloads ([eb5de2c](https://github.com/hagopj13/node-express-boilerplate/commit/eb5de2c7523ac166ca933bff83ef1e87274f3478)), closes [#28](https://github.com/hagopj13/node-express-boilerplate/issues/28)\n\n## [1.4.0](https://github.com/hagopj13/node-express-boilerplate/compare/v1.3.0...v1.4.0) (2020-08-22)\n\n### Features\n\n- use native functions instead of Lodash ([66c9e33](https://github.com/hagopj13/node-express-boilerplate/commit/66c9e33d65c88989634fc485e89b396645670730)), closes [#18](https://github.com/hagopj13/node-express-boilerplate/issues/18)\n- add logout endpoint ([750feb5](https://github.com/hagopj13/node-express-boilerplate/commit/750feb5b1ddadb4da6742b445cdb1112a615ace4)), closes [#19](https://github.com/hagopj13/node-express-boilerplate/issues/19)\n\n## [1.3.0](https://github.com/hagopj13/node-express-boilerplate/compare/v1.2.0...v1.3.0) (2020-05-17)\n\n### Features\n\n- add toJSON custom mongoose schema plugin ([f8ba3f6](https://github.com/hagopj13/node-express-boilerplate/commit/f8ba3f619ac42f2030c358fb44095b72fb37013b))\n- add paginate custom mongoose schema plugin ([97fef4c](https://github.com/hagopj13/node-express-boilerplate/commit/97fef4cac91c86e4d33e9010705775fa9f160e96)), closes [#13](https://github.com/hagopj13/node-express-boilerplate/issues/13)\n\n## [1.2.0](https://github.com/hagopj13/node-express-boilerplate/compare/v1.1.3...v1.2.0) (2020-05-13)\n\n### Features\n\n- add api documentation ([#12](https://github.com/hagopj13/node-express-boilerplate/pull/12)) ([0777889](https://github.com/hagopj13/node-express-boilerplate/commit/07778894b706ef94e35f87046db112b39b58316c)), closes [#3](https://github.com/hagopj13/node-express-boilerplate/issues/3)\n\n### Bug Fixes\n\n- run app with a non-root user inside docker ([#10](https://github.com/hagopj13/node-express-boilerplate/pull/10)) ([1e3195d](https://github.com/hagopj13/node-express-boilerplate/commit/1e3195d547510d51804028d4ab447cbc53372e48))\n\n## [1.1.3](https://github.com/hagopj13/node-express-boilerplate/compare/v1.1.2...v1.1.3) (2020-03-14)\n\n### Bug Fixes\n\n- fix vulnerability issues by upgrading dependencies ([9c15650](https://github.com/hagopj13/node-express-boilerplate/commit/9c15650acfb0d991b621abc60ba534c904fd3fd1))\n\n## [1.1.2](https://github.com/hagopj13/node-express-boilerplate/compare/v1.1.1...v1.1.2) (2020-02-16)\n\n### Bug Fixes\n\n- fix issue with incorrect stack for errors that are not of type AppError ([48d1a5a](https://github.com/hagopj13/node-express-boilerplate/commit/48d1a5ada5e5fe0975a17b521d3d7a6e1f4cab3b))\n\n## [1.1.1](https://github.com/hagopj13/node-express-boilerplate/compare/v1.1.0...v1.1.1) (2019-12-04)\n\n### Bug Fixes\n\n- use JWT iat as seconds from epoch instead of milliseconds ([#4](https://github.com/hagopj13/node-express-boilerplate/pull/4)) ([c4e1a84](https://github.com/hagopj13/node-express-boilerplate/commit/c4e1a8487c6d41cc20944a081a13a2a1990de0cd))\n\n## [1.1.0](https://github.com/hagopj13/node-express-boilerplate/compare/v1.0.0...v1.1.0) (2019-11-23)\n\n### Features\n\n- add docker support ([3401449](https://github.com/hagopj13/node-express-boilerplate/commit/340144979cf5e84abb047a891a0b908b01af3645)), closes [#2](https://github.com/hagopj13/node-express-boilerplate/issues/2)\n- verify connection to email server at startup ([f38d86a](https://github.com/hagopj13/node-express-boilerplate/commit/f38d86a181f1816d720e009aa94619e25ef4bf93))\n\n## 1.0.0 (2019-11-22)\n\n### Features\n\n- initial release\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at hagopj13@gmail.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nFirst off, thank you so much for taking the time to contribute. All contributions are more than welcome!\n\n## How can I contribute?\n\nIf 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:\n\n- **Create an issue** to explain and discuss the details\n- **Fork the repo**\n- **Clone the repo** and set it up (check out the [manual installation](https://github.com/hagopj13/node-express-boilerplate#manual-installation) section in README.md)\n- **Implement** the necessary changes\n- **Create tests** to keep the code coverage high\n- **Send a pull request**\n\n## Guidelines\n\n### Git commit messages\n\n- Limit the subject line to 72 characters\n- Capitalize the first letter of the subject line\n- Use the present tense (\"Add feature\" instead of \"Added feature\")\n- Separate the subject from the body with a blank line\n- Reference issues and pull requests in the body\n\n### Coding style guide\n\nWe 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).\n\nSome 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.\n\nPlease make sure that the code you are pushing conforms to the style guides mentioned above.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:alpine\n\nRUN mkdir -p /usr/src/node-app && chown -R node:node /usr/src/node-app\n\nWORKDIR /usr/src/node-app\n\nCOPY package.json yarn.lock ./\n\nUSER node\n\nRUN yarn install --pure-lockfile\n\nCOPY --chown=node:node . .\n\nEXPOSE 3000\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Hagop Jamkojian\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# RESTful API Node Server Boilerplate\n\n[![Build Status](https://travis-ci.org/hagopj13/node-express-boilerplate.svg?branch=master)](https://travis-ci.org/hagopj13/node-express-boilerplate)\n[![Coverage Status](https://coveralls.io/repos/github/hagopj13/node-express-boilerplate/badge.svg?branch=master)](https://coveralls.io/github/hagopj13/node-express-boilerplate?branch=master)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)\n\nA boilerplate/starter project for quickly building RESTful APIs using Node.js, Express, and Mongoose.\n\nBy running a single command, you will get a production-ready Node.js app installed and fully configured on your machine. The app comes with many built-in features, such as authentication using JWT, request validation, unit and integration tests, continuous integration, docker support, API documentation, pagination, etc. For more details, check the features list below.\n\n## Quick Start\n\nTo create a project, simply run:\n\n```bash\nnpx create-nodejs-express-app <project-name>\n```\n\nOr\n\n```bash\nnpm init nodejs-express-app <project-name>\n```\n\n## Manual Installation\n\nIf you would still prefer to do the installation manually, follow these steps:\n\nClone the repo:\n\n```bash\ngit clone --depth 1 https://github.com/hagopj13/node-express-boilerplate.git\ncd node-express-boilerplate\nnpx rimraf ./.git\n```\n\nInstall the dependencies:\n\n```bash\nyarn install\n```\n\nSet the environment variables:\n\n```bash\ncp .env.example .env\n\n# open .env and modify the environment variables (if needed)\n```\n\n## Table of Contents\n\n- [Features](#features)\n- [Commands](#commands)\n- [Environment Variables](#environment-variables)\n- [Project Structure](#project-structure)\n- [API Documentation](#api-documentation)\n- [Error Handling](#error-handling)\n- [Validation](#validation)\n- [Authentication](#authentication)\n- [Authorization](#authorization)\n- [Logging](#logging)\n- [Custom Mongoose Plugins](#custom-mongoose-plugins)\n- [Linting](#linting)\n- [Contributing](#contributing)\n\n## Features\n\n- **NoSQL database**: [MongoDB](https://www.mongodb.com) object data modeling using [Mongoose](https://mongoosejs.com)\n- **Authentication and authorization**: using [passport](http://www.passportjs.org)\n- **Validation**: request data validation using [Joi](https://github.com/hapijs/joi)\n- **Logging**: using [winston](https://github.com/winstonjs/winston) and [morgan](https://github.com/expressjs/morgan)\n- **Testing**: unit and integration tests using [Jest](https://jestjs.io)\n- **Error handling**: centralized error handling mechanism\n- **API documentation**: with [swagger-jsdoc](https://github.com/Surnet/swagger-jsdoc) and [swagger-ui-express](https://github.com/scottie1984/swagger-ui-express)\n- **Process management**: advanced production process management using [PM2](https://pm2.keymetrics.io)\n- **Dependency management**: with [Yarn](https://yarnpkg.com)\n- **Environment variables**: using [dotenv](https://github.com/motdotla/dotenv) and [cross-env](https://github.com/kentcdodds/cross-env#readme)\n- **Security**: set security HTTP headers using [helmet](https://helmetjs.github.io)\n- **Santizing**: sanitize request data against xss and query injection\n- **CORS**: Cross-Origin Resource-Sharing enabled using [cors](https://github.com/expressjs/cors)\n- **Compression**: gzip compression with [compression](https://github.com/expressjs/compression)\n- **CI**: continuous integration with [Travis CI](https://travis-ci.org)\n- **Docker support**\n- **Code coverage**: using [coveralls](https://coveralls.io)\n- **Code quality**: with [Codacy](https://www.codacy.com)\n- **Git hooks**: with [husky](https://github.com/typicode/husky) and [lint-staged](https://github.com/okonet/lint-staged)\n- **Linting**: with [ESLint](https://eslint.org) and [Prettier](https://prettier.io)\n- **Editor config**: consistent editor configuration using [EditorConfig](https://editorconfig.org)\n\n## Commands\n\nRunning locally:\n\n```bash\nyarn dev\n```\n\nRunning in production:\n\n```bash\nyarn start\n```\n\nTesting:\n\n```bash\n# run all tests\nyarn test\n\n# run all tests in watch mode\nyarn test:watch\n\n# run test coverage\nyarn coverage\n```\n\nDocker:\n\n```bash\n# run docker container in development mode\nyarn docker:dev\n\n# run docker container in production mode\nyarn docker:prod\n\n# run all tests in a docker container\nyarn docker:test\n```\n\nLinting:\n\n```bash\n# run ESLint\nyarn lint\n\n# fix ESLint errors\nyarn lint:fix\n\n# run prettier\nyarn prettier\n\n# fix prettier errors\nyarn prettier:fix\n```\n\n## Environment Variables\n\nThe environment variables can be found and modified in the `.env` file. They come with these default values:\n\n```bash\n# Port number\nPORT=3000\n\n# URL of the Mongo DB\nMONGODB_URL=mongodb://127.0.0.1:27017/node-boilerplate\n\n# JWT\n# JWT secret key\nJWT_SECRET=thisisasamplesecret\n# Number of minutes after which an access token expires\nJWT_ACCESS_EXPIRATION_MINUTES=30\n# Number of days after which a refresh token expires\nJWT_REFRESH_EXPIRATION_DAYS=30\n\n# SMTP configuration options for the email service\n# For testing, you can use a fake SMTP service like Ethereal: https://ethereal.email/create\nSMTP_HOST=email-server\nSMTP_PORT=587\nSMTP_USERNAME=email-server-username\nSMTP_PASSWORD=email-server-password\nEMAIL_FROM=support@yourapp.com\n```\n\n## Project Structure\n\n```\nsrc\\\n |--config\\         # Environment variables and configuration related things\n |--controllers\\    # Route controllers (controller layer)\n |--docs\\           # Swagger files\n |--middlewares\\    # Custom express middlewares\n |--models\\         # Mongoose models (data layer)\n |--routes\\         # Routes\n |--services\\       # Business logic (service layer)\n |--utils\\          # Utility classes and functions\n |--validations\\    # Request data validation schemas\n |--app.js          # Express app\n |--index.js        # App entry point\n```\n\n## API Documentation\n\nTo view the list of available APIs and their specifications, run the server and go to `http://localhost:3000/v1/docs` in your browser. This documentation page is automatically generated using the [swagger](https://swagger.io/) definitions written as comments in the route files.\n\n### API Endpoints\n\nList of available routes:\n\n**Auth routes**:\\\n`POST /v1/auth/register` - register\\\n`POST /v1/auth/login` - login\\\n`POST /v1/auth/refresh-tokens` - refresh auth tokens\\\n`POST /v1/auth/forgot-password` - send reset password email\\\n`POST /v1/auth/reset-password` - reset password\\\n`POST /v1/auth/send-verification-email` - send verification email\\\n`POST /v1/auth/verify-email` - verify email\n\n**User routes**:\\\n`POST /v1/users` - create a user\\\n`GET /v1/users` - get all users\\\n`GET /v1/users/:userId` - get user\\\n`PATCH /v1/users/:userId` - update user\\\n`DELETE /v1/users/:userId` - delete user\n\n## Error Handling\n\nThe app has a centralized error handling mechanism.\n\nControllers should try to catch the errors and forward them to the error handling middleware (by calling `next(error)`). For convenience, you can also wrap the controller inside the catchAsync utility wrapper, which forwards the error.\n\n```javascript\nconst catchAsync = require('../utils/catchAsync');\n\nconst controller = catchAsync(async (req, res) => {\n  // this error will be forwarded to the error handling middleware\n  throw new Error('Something wrong happened');\n});\n```\n\nThe error handling middleware sends an error response, which has the following format:\n\n```json\n{\n  \"code\": 404,\n  \"message\": \"Not found\"\n}\n```\n\nWhen running in development mode, the error response also contains the error stack.\n\nThe app has a utility ApiError class to which you can attach a response code and a message, and then throw it from anywhere (catchAsync will catch it).\n\nFor example, if you are trying to get a user from the DB who is not found, and you want to send a 404 error, the code should look something like:\n\n```javascript\nconst httpStatus = require('http-status');\nconst ApiError = require('../utils/ApiError');\nconst User = require('../models/User');\n\nconst getUser = async (userId) => {\n  const user = await User.findById(userId);\n  if (!user) {\n    throw new ApiError(httpStatus.NOT_FOUND, 'User not found');\n  }\n};\n```\n\n## Validation\n\nRequest data is validated using [Joi](https://joi.dev/). Check the [documentation](https://joi.dev/api/) for more details on how to write Joi validation schemas.\n\nThe validation schemas are defined in the `src/validations` directory and are used in the routes by providing them as parameters to the `validate` middleware.\n\n```javascript\nconst express = require('express');\nconst validate = require('../../middlewares/validate');\nconst userValidation = require('../../validations/user.validation');\nconst userController = require('../../controllers/user.controller');\n\nconst router = express.Router();\n\nrouter.post('/users', validate(userValidation.createUser), userController.createUser);\n```\n\n## Authentication\n\nTo require authentication for certain routes, you can use the `auth` middleware.\n\n```javascript\nconst express = require('express');\nconst auth = require('../../middlewares/auth');\nconst userController = require('../../controllers/user.controller');\n\nconst router = express.Router();\n\nrouter.post('/users', auth(), userController.createUser);\n```\n\nThese routes require a valid JWT access token in the Authorization request header using the Bearer schema. If the request does not contain a valid access token, an Unauthorized (401) error is thrown.\n\n**Generating Access Tokens**:\n\nAn access token can be generated by making a successful call to the register (`POST /v1/auth/register`) or login (`POST /v1/auth/login`) endpoints. The response of these endpoints also contains refresh tokens (explained below).\n\nAn access token is valid for 30 minutes. You can modify this expiration time by changing the `JWT_ACCESS_EXPIRATION_MINUTES` environment variable in the .env file.\n\n**Refreshing Access Tokens**:\n\nAfter the access token expires, a new access token can be generated, by making a call to the refresh token endpoint (`POST /v1/auth/refresh-tokens`) and sending along a valid refresh token in the request body. This call returns a new access token and a new refresh token.\n\nA refresh token is valid for 30 days. You can modify this expiration time by changing the `JWT_REFRESH_EXPIRATION_DAYS` environment variable in the .env file.\n\n## Authorization\n\nThe `auth` middleware can also be used to require certain rights/permissions to access a route.\n\n```javascript\nconst express = require('express');\nconst auth = require('../../middlewares/auth');\nconst userController = require('../../controllers/user.controller');\n\nconst router = express.Router();\n\nrouter.post('/users', auth('manageUsers'), userController.createUser);\n```\n\nIn the example above, an authenticated user can access this route only if that user has the `manageUsers` permission.\n\nThe permissions are role-based. You can view the permissions/rights of each role in the `src/config/roles.js` file.\n\nIf the user making the request does not have the required permissions to access this route, a Forbidden (403) error is thrown.\n\n## Logging\n\nImport the logger from `src/config/logger.js`. It is using the [Winston](https://github.com/winstonjs/winston) logging library.\n\nLogging should be done according to the following severity levels (ascending order from most important to least important):\n\n```javascript\nconst logger = require('<path to src>/config/logger');\n\nlogger.error('message'); // level 0\nlogger.warn('message'); // level 1\nlogger.info('message'); // level 2\nlogger.http('message'); // level 3\nlogger.verbose('message'); // level 4\nlogger.debug('message'); // level 5\n```\n\nIn development mode, log messages of all severity levels will be printed to the console.\n\nIn production mode, only `info`, `warn`, and `error` logs will be printed to the console.\\\nIt is up to the server (or process manager) to actually read them from the console and store them in log files.\\\nThis app uses pm2 in production mode, which is already configured to store the logs in log files.\n\nNote: API request information (request url, response code, timestamp, etc.) are also automatically logged (using [morgan](https://github.com/expressjs/morgan)).\n\n## Custom Mongoose Plugins\n\nThe app also contains 2 custom mongoose plugins that you can attach to any mongoose model schema. You can find the plugins in `src/models/plugins`.\n\n```javascript\nconst mongoose = require('mongoose');\nconst { toJSON, paginate } = require('./plugins');\n\nconst userSchema = mongoose.Schema(\n  {\n    /* schema definition here */\n  },\n  { timestamps: true }\n);\n\nuserSchema.plugin(toJSON);\nuserSchema.plugin(paginate);\n\nconst User = mongoose.model('User', userSchema);\n```\n\n### toJSON\n\nThe toJSON plugin applies the following changes in the toJSON transform call:\n\n- removes \\_\\_v, createdAt, updatedAt, and any schema path that has private: true\n- replaces \\_id with id\n\n### paginate\n\nThe paginate plugin adds the `paginate` static method to the mongoose schema.\n\nAdding this plugin to the `User` model schema will allow you to do the following:\n\n```javascript\nconst queryUsers = async (filter, options) => {\n  const users = await User.paginate(filter, options);\n  return users;\n};\n```\n\nThe `filter` param is a regular mongo filter.\n\nThe `options` param can have the following (optional) fields:\n\n```javascript\nconst options = {\n  sortBy: 'name:desc', // sort order\n  limit: 5, // maximum results per page\n  page: 2, // page number\n};\n```\n\nThe plugin also supports sorting by multiple criteria (separated by a comma): `sortBy: name:desc,role:asc`\n\nThe `paginate` method returns a Promise, which fulfills with an object having the following properties:\n\n```json\n{\n  \"results\": [],\n  \"page\": 2,\n  \"limit\": 5,\n  \"totalPages\": 10,\n  \"totalResults\": 48\n}\n```\n\n## Linting\n\nLinting is done using [ESLint](https://eslint.org/) and [Prettier](https://prettier.io).\n\nIn this app, ESLint is configured to follow the [Airbnb JavaScript style guide](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb-base) with some modifications. It also extends [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) to turn off all rules that are unnecessary or might conflict with Prettier.\n\nTo modify the ESLint configuration, update the `.eslintrc.json` file. To modify the Prettier configuration, update the `.prettierrc.json` file.\n\nTo prevent a certain file or directory from being linted, add it to `.eslintignore` and `.prettierignore`.\n\nTo maintain a consistent coding style across different IDEs, the project contains `.editorconfig`\n\n## Contributing\n\nContributions are more than welcome! Please check out the [contributing guide](CONTRIBUTING.md).\n\n## Inspirations\n\n- [danielfsousa/express-rest-es2017-boilerplate](https://github.com/danielfsousa/express-rest-es2017-boilerplate)\n- [madhums/node-express-mongoose](https://github.com/madhums/node-express-mongoose)\n- [kunalkapadia/express-mongoose-es6-rest-api](https://github.com/kunalkapadia/express-mongoose-es6-rest-api)\n\n## License\n\n[MIT](LICENSE)\n"
  },
  {
    "path": "bin/createNodejsApp.js",
    "content": "#!/usr/bin/env node\nconst util = require('util');\nconst path = require('path');\nconst fs = require('fs');\nconst { execSync } = require('child_process');\n\n// Utility functions\nconst exec = util.promisify(require('child_process').exec);\nasync function runCmd(command) {\n  try {\n    const { stdout, stderr } = await exec(command);\n    console.log(stdout);\n    console.log(stderr);\n  } catch {\n    (error) => {\n      console.log(error);\n    };\n  }\n}\n\nasync function hasYarn() {\n  try {\n    await execSync('yarnpkg --version', { stdio: 'ignore' });\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n// Validate arguments\nif (process.argv.length < 3) {\n  console.log('Please specify the target project directory.');\n  console.log('For example:');\n  console.log('    npx create-nodejs-app my-app');\n  console.log('    OR');\n  console.log('    npm init nodejs-app my-app');\n  process.exit(1);\n}\n\n// Define constants\nconst ownPath = process.cwd();\nconst folderName = process.argv[2];\nconst appPath = path.join(ownPath, folderName);\nconst repo = 'https://github.com/hagopj13/node-express-boilerplate.git';\n\n// Check if directory already exists\ntry {\n  fs.mkdirSync(appPath);\n} catch (err) {\n  if (err.code === 'EEXIST') {\n    console.log('Directory already exists. Please choose another name for the project.');\n  } else {\n    console.log(err);\n  }\n  process.exit(1);\n}\n\nasync function setup() {\n  try {\n    // Clone repo\n    console.log(`Downloading files from repo ${repo}`);\n    await runCmd(`git clone --depth 1 ${repo} ${folderName}`);\n    console.log('Cloned successfully.');\n    console.log('');\n\n    // Change directory\n    process.chdir(appPath);\n\n    // Install dependencies\n    const useYarn = await hasYarn();\n    console.log('Installing dependencies...');\n    if (useYarn) {\n      await runCmd('yarn install');\n    } else {\n      await runCmd('npm install');\n    }\n    console.log('Dependencies installed successfully.');\n    console.log();\n\n    // Copy envornment variables\n    fs.copyFileSync(path.join(appPath, '.env.example'), path.join(appPath, '.env'));\n    console.log('Environment files copied.');\n\n    // Delete .git folder\n    await runCmd('npx rimraf ./.git');\n\n    // Remove extra files\n    fs.unlinkSync(path.join(appPath, 'CHANGELOG.md'));\n    fs.unlinkSync(path.join(appPath, 'CODE_OF_CONDUCT.md'));\n    fs.unlinkSync(path.join(appPath, 'CONTRIBUTING.md'));\n    fs.unlinkSync(path.join(appPath, 'bin', 'createNodejsApp.js'));\n    fs.rmdirSync(path.join(appPath, 'bin'));\n    if (!useYarn) {\n      fs.unlinkSync(path.join(appPath, 'yarn.lock'));\n    }\n\n    console.log('Installation is now complete!');\n    console.log();\n\n    console.log('We suggest that you start by typing:');\n    console.log(`    cd ${folderName}`);\n    console.log(useYarn ? '    yarn dev' : '    npm run dev');\n    console.log();\n    console.log('Enjoy your production-ready Node.js app, which already supports a large number of ready-made features!');\n    console.log('Check README.md for more info.');\n  } catch (error) {\n    console.log(error);\n  }\n}\n\nsetup();\n"
  },
  {
    "path": "docker-compose.dev.yml",
    "content": "version: '3'\n\nservices:\n  node-app:\n    container_name: node-app-dev\n    command: yarn dev -L\n"
  },
  {
    "path": "docker-compose.prod.yml",
    "content": "version: '3'\n\nservices:\n  node-app:\n    container_name: node-app-prod\n    command: yarn start\n"
  },
  {
    "path": "docker-compose.test.yml",
    "content": "version: '3'\n\nservices:\n  node-app:\n    container_name: node-app-test\n    command: yarn test\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\n\nservices:\n  node-app:\n    build: .\n    image: node-app\n    environment:\n      - MONGODB_URL=mongodb://mongodb:27017/node-boilerplate\n    ports:\n      - '3000:3000'\n    depends_on:\n      - mongodb\n    volumes:\n      - .:/usr/src/node-app\n    networks:\n      - node-network\n\n  mongodb:\n    image: mongo:4.2.1-bionic\n    ports:\n      - '27017:27017'\n    volumes:\n      - dbdata:/data/db\n    networks:\n      - node-network\n\nvolumes:\n  dbdata:\n\nnetworks:\n  node-network:\n    driver: bridge\n"
  },
  {
    "path": "ecosystem.config.json",
    "content": "{\n  \"apps\": [\n    {\n      \"name\": \"app\",\n      \"script\": \"src/index.js\",\n      \"instances\": 1,\n      \"autorestart\": true,\n      \"watch\": false,\n      \"time\": true,\n      \"env\": {\n        \"NODE_ENV\": \"production\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  testEnvironment: 'node',\n  testEnvironmentOptions: {\n    NODE_ENV: 'test',\n  },\n  restoreMocks: true,\n  coveragePathIgnorePatterns: ['node_modules', 'src/config', 'src/app.js', 'tests'],\n  coverageReporters: ['text', 'lcov', 'clover', 'html'],\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"create-nodejs-express-app\",\n  \"version\": \"1.7.0\",\n  \"description\": \"Create a Node.js app for building production-ready RESTful APIs using Express, by running one command\",\n  \"bin\": \"bin/createNodejsApp.js\",\n  \"main\": \"src/index.js\",\n  \"repository\": \"https://github.com/hagopj13/node-express-boilerplate.git\",\n  \"author\": \"Hagop Jamkojian <hagopj13@gmail.com>\",\n  \"license\": \"MIT\",\n  \"engines\": {\n    \"node\": \">=12.0.0\"\n  },\n  \"scripts\": {\n    \"start\": \"pm2 start ecosystem.config.json --no-daemon\",\n    \"dev\": \"cross-env NODE_ENV=development nodemon src/index.js\",\n    \"test\": \"jest -i --colors --verbose --detectOpenHandles\",\n    \"test:watch\": \"jest -i --watchAll\",\n    \"coverage\": \"jest -i --coverage\",\n    \"coverage:coveralls\": \"jest -i --coverage --coverageReporters=text-lcov | coveralls\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"prettier\": \"prettier --check **/*.js\",\n    \"prettier:fix\": \"prettier --write **/*.js\",\n    \"docker:prod\": \"docker-compose -f docker-compose.yml -f docker-compose.prod.yml up\",\n    \"docker:dev\": \"docker-compose -f docker-compose.yml -f docker-compose.dev.yml up\",\n    \"docker:test\": \"docker-compose -f docker-compose.yml -f docker-compose.test.yml up\",\n    \"prepare\": \"husky install\"\n  },\n  \"keywords\": [\n    \"node\",\n    \"node.js\",\n    \"boilerplate\",\n    \"generator\",\n    \"express\",\n    \"rest\",\n    \"api\",\n    \"mongodb\",\n    \"mongoose\",\n    \"es6\",\n    \"es7\",\n    \"es8\",\n    \"es9\",\n    \"jest\",\n    \"travis\",\n    \"docker\",\n    \"passport\",\n    \"joi\",\n    \"eslint\",\n    \"prettier\"\n  ],\n  \"dependencies\": {\n    \"bcryptjs\": \"^2.4.3\",\n    \"compression\": \"^1.7.4\",\n    \"cors\": \"^2.8.5\",\n    \"cross-env\": \"^7.0.0\",\n    \"dotenv\": \"^10.0.0\",\n    \"express\": \"^4.17.1\",\n    \"express-mongo-sanitize\": \"^2.0.0\",\n    \"express-rate-limit\": \"^5.0.0\",\n    \"helmet\": \"^4.1.0\",\n    \"http-status\": \"^1.4.0\",\n    \"joi\": \"^17.3.0\",\n    \"jsonwebtoken\": \"^8.5.1\",\n    \"moment\": \"^2.24.0\",\n    \"mongoose\": \"^5.7.7\",\n    \"morgan\": \"^1.9.1\",\n    \"nodemailer\": \"^6.3.1\",\n    \"passport\": \"^0.4.0\",\n    \"passport-jwt\": \"^4.0.0\",\n    \"pm2\": \"^5.1.0\",\n    \"swagger-jsdoc\": \"^6.0.8\",\n    \"swagger-ui-express\": \"^4.1.6\",\n    \"validator\": \"^13.0.0\",\n    \"winston\": \"^3.2.1\",\n    \"xss-clean\": \"^0.1.1\"\n  },\n  \"devDependencies\": {\n    \"coveralls\": \"^3.0.7\",\n    \"eslint\": \"^7.0.0\",\n    \"eslint-config-airbnb-base\": \"^14.0.0\",\n    \"eslint-config-prettier\": \"^8.1.0\",\n    \"eslint-plugin-import\": \"^2.18.2\",\n    \"eslint-plugin-jest\": \"^24.0.1\",\n    \"eslint-plugin-prettier\": \"^3.1.1\",\n    \"eslint-plugin-security\": \"^1.4.0\",\n    \"faker\": \"^5.1.0\",\n    \"husky\": \"7.0.4\",\n    \"jest\": \"^26.0.1\",\n    \"lint-staged\": \"^11.0.0\",\n    \"node-mocks-http\": \"^1.8.0\",\n    \"nodemon\": \"^2.0.0\",\n    \"prettier\": \"^2.0.5\",\n    \"supertest\": \"^6.0.1\"\n  }\n}\n"
  },
  {
    "path": "src/app.js",
    "content": "const express = require('express');\nconst helmet = require('helmet');\nconst xss = require('xss-clean');\nconst mongoSanitize = require('express-mongo-sanitize');\nconst compression = require('compression');\nconst cors = require('cors');\nconst passport = require('passport');\nconst httpStatus = require('http-status');\nconst config = require('./config/config');\nconst morgan = require('./config/morgan');\nconst { jwtStrategy } = require('./config/passport');\nconst { authLimiter } = require('./middlewares/rateLimiter');\nconst routes = require('./routes/v1');\nconst { errorConverter, errorHandler } = require('./middlewares/error');\nconst ApiError = require('./utils/ApiError');\n\nconst app = express();\n\nif (config.env !== 'test') {\n  app.use(morgan.successHandler);\n  app.use(morgan.errorHandler);\n}\n\n// set security HTTP headers\napp.use(helmet());\n\n// parse json request body\napp.use(express.json());\n\n// parse urlencoded request body\napp.use(express.urlencoded({ extended: true }));\n\n// sanitize request data\napp.use(xss());\napp.use(mongoSanitize());\n\n// gzip compression\napp.use(compression());\n\n// enable cors\napp.use(cors());\napp.options('*', cors());\n\n// jwt authentication\napp.use(passport.initialize());\npassport.use('jwt', jwtStrategy);\n\n// limit repeated failed requests to auth endpoints\nif (config.env === 'production') {\n  app.use('/v1/auth', authLimiter);\n}\n\n// v1 api routes\napp.use('/v1', routes);\n\n// send back a 404 error for any unknown api request\napp.use((req, res, next) => {\n  next(new ApiError(httpStatus.NOT_FOUND, 'Not found'));\n});\n\n// convert error to ApiError, if needed\napp.use(errorConverter);\n\n// handle error\napp.use(errorHandler);\n\nmodule.exports = app;\n"
  },
  {
    "path": "src/config/config.js",
    "content": "const dotenv = require('dotenv');\nconst path = require('path');\nconst Joi = require('joi');\n\ndotenv.config({ path: path.join(__dirname, '../../.env') });\n\nconst envVarsSchema = Joi.object()\n  .keys({\n    NODE_ENV: Joi.string().valid('production', 'development', 'test').required(),\n    PORT: Joi.number().default(3000),\n    MONGODB_URL: Joi.string().required().description('Mongo DB url'),\n    JWT_SECRET: Joi.string().required().description('JWT secret key'),\n    JWT_ACCESS_EXPIRATION_MINUTES: Joi.number().default(30).description('minutes after which access tokens expire'),\n    JWT_REFRESH_EXPIRATION_DAYS: Joi.number().default(30).description('days after which refresh tokens expire'),\n    JWT_RESET_PASSWORD_EXPIRATION_MINUTES: Joi.number()\n      .default(10)\n      .description('minutes after which reset password token expires'),\n    JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: Joi.number()\n      .default(10)\n      .description('minutes after which verify email token expires'),\n    SMTP_HOST: Joi.string().description('server that will send the emails'),\n    SMTP_PORT: Joi.number().description('port to connect to the email server'),\n    SMTP_USERNAME: Joi.string().description('username for email server'),\n    SMTP_PASSWORD: Joi.string().description('password for email server'),\n    EMAIL_FROM: Joi.string().description('the from field in the emails sent by the app'),\n  })\n  .unknown();\n\nconst { value: envVars, error } = envVarsSchema.prefs({ errors: { label: 'key' } }).validate(process.env);\n\nif (error) {\n  throw new Error(`Config validation error: ${error.message}`);\n}\n\nmodule.exports = {\n  env: envVars.NODE_ENV,\n  port: envVars.PORT,\n  mongoose: {\n    url: envVars.MONGODB_URL + (envVars.NODE_ENV === 'test' ? '-test' : ''),\n    options: {\n      useCreateIndex: true,\n      useNewUrlParser: true,\n      useUnifiedTopology: true,\n    },\n  },\n  jwt: {\n    secret: envVars.JWT_SECRET,\n    accessExpirationMinutes: envVars.JWT_ACCESS_EXPIRATION_MINUTES,\n    refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,\n    resetPasswordExpirationMinutes: envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,\n    verifyEmailExpirationMinutes: envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,\n  },\n  email: {\n    smtp: {\n      host: envVars.SMTP_HOST,\n      port: envVars.SMTP_PORT,\n      auth: {\n        user: envVars.SMTP_USERNAME,\n        pass: envVars.SMTP_PASSWORD,\n      },\n    },\n    from: envVars.EMAIL_FROM,\n  },\n};\n"
  },
  {
    "path": "src/config/logger.js",
    "content": "const winston = require('winston');\nconst config = require('./config');\n\nconst enumerateErrorFormat = winston.format((info) => {\n  if (info instanceof Error) {\n    Object.assign(info, { message: info.stack });\n  }\n  return info;\n});\n\nconst logger = winston.createLogger({\n  level: config.env === 'development' ? 'debug' : 'info',\n  format: winston.format.combine(\n    enumerateErrorFormat(),\n    config.env === 'development' ? winston.format.colorize() : winston.format.uncolorize(),\n    winston.format.splat(),\n    winston.format.printf(({ level, message }) => `${level}: ${message}`)\n  ),\n  transports: [\n    new winston.transports.Console({\n      stderrLevels: ['error'],\n    }),\n  ],\n});\n\nmodule.exports = logger;\n"
  },
  {
    "path": "src/config/morgan.js",
    "content": "const morgan = require('morgan');\nconst config = require('./config');\nconst logger = require('./logger');\n\nmorgan.token('message', (req, res) => res.locals.errorMessage || '');\n\nconst getIpFormat = () => (config.env === 'production' ? ':remote-addr - ' : '');\nconst successResponseFormat = `${getIpFormat()}:method :url :status - :response-time ms`;\nconst errorResponseFormat = `${getIpFormat()}:method :url :status - :response-time ms - message: :message`;\n\nconst successHandler = morgan(successResponseFormat, {\n  skip: (req, res) => res.statusCode >= 400,\n  stream: { write: (message) => logger.info(message.trim()) },\n});\n\nconst errorHandler = morgan(errorResponseFormat, {\n  skip: (req, res) => res.statusCode < 400,\n  stream: { write: (message) => logger.error(message.trim()) },\n});\n\nmodule.exports = {\n  successHandler,\n  errorHandler,\n};\n"
  },
  {
    "path": "src/config/passport.js",
    "content": "const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');\nconst config = require('./config');\nconst { tokenTypes } = require('./tokens');\nconst { User } = require('../models');\n\nconst jwtOptions = {\n  secretOrKey: config.jwt.secret,\n  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\n};\n\nconst jwtVerify = async (payload, done) => {\n  try {\n    if (payload.type !== tokenTypes.ACCESS) {\n      throw new Error('Invalid token type');\n    }\n    const user = await User.findById(payload.sub);\n    if (!user) {\n      return done(null, false);\n    }\n    done(null, user);\n  } catch (error) {\n    done(error, false);\n  }\n};\n\nconst jwtStrategy = new JwtStrategy(jwtOptions, jwtVerify);\n\nmodule.exports = {\n  jwtStrategy,\n};\n"
  },
  {
    "path": "src/config/roles.js",
    "content": "const allRoles = {\n  user: [],\n  admin: ['getUsers', 'manageUsers'],\n};\n\nconst roles = Object.keys(allRoles);\nconst roleRights = new Map(Object.entries(allRoles));\n\nmodule.exports = {\n  roles,\n  roleRights,\n};\n"
  },
  {
    "path": "src/config/tokens.js",
    "content": "const tokenTypes = {\n  ACCESS: 'access',\n  REFRESH: 'refresh',\n  RESET_PASSWORD: 'resetPassword',\n  VERIFY_EMAIL: 'verifyEmail',\n};\n\nmodule.exports = {\n  tokenTypes,\n};\n"
  },
  {
    "path": "src/controllers/auth.controller.js",
    "content": "const httpStatus = require('http-status');\nconst catchAsync = require('../utils/catchAsync');\nconst { authService, userService, tokenService, emailService } = require('../services');\n\nconst register = catchAsync(async (req, res) => {\n  const user = await userService.createUser(req.body);\n  const tokens = await tokenService.generateAuthTokens(user);\n  res.status(httpStatus.CREATED).send({ user, tokens });\n});\n\nconst login = catchAsync(async (req, res) => {\n  const { email, password } = req.body;\n  const user = await authService.loginUserWithEmailAndPassword(email, password);\n  const tokens = await tokenService.generateAuthTokens(user);\n  res.send({ user, tokens });\n});\n\nconst logout = catchAsync(async (req, res) => {\n  await authService.logout(req.body.refreshToken);\n  res.status(httpStatus.NO_CONTENT).send();\n});\n\nconst refreshTokens = catchAsync(async (req, res) => {\n  const tokens = await authService.refreshAuth(req.body.refreshToken);\n  res.send({ ...tokens });\n});\n\nconst forgotPassword = catchAsync(async (req, res) => {\n  const resetPasswordToken = await tokenService.generateResetPasswordToken(req.body.email);\n  await emailService.sendResetPasswordEmail(req.body.email, resetPasswordToken);\n  res.status(httpStatus.NO_CONTENT).send();\n});\n\nconst resetPassword = catchAsync(async (req, res) => {\n  await authService.resetPassword(req.query.token, req.body.password);\n  res.status(httpStatus.NO_CONTENT).send();\n});\n\nconst sendVerificationEmail = catchAsync(async (req, res) => {\n  const verifyEmailToken = await tokenService.generateVerifyEmailToken(req.user);\n  await emailService.sendVerificationEmail(req.user.email, verifyEmailToken);\n  res.status(httpStatus.NO_CONTENT).send();\n});\n\nconst verifyEmail = catchAsync(async (req, res) => {\n  await authService.verifyEmail(req.query.token);\n  res.status(httpStatus.NO_CONTENT).send();\n});\n\nmodule.exports = {\n  register,\n  login,\n  logout,\n  refreshTokens,\n  forgotPassword,\n  resetPassword,\n  sendVerificationEmail,\n  verifyEmail,\n};\n"
  },
  {
    "path": "src/controllers/index.js",
    "content": "module.exports.authController = require('./auth.controller');\nmodule.exports.userController = require('./user.controller');\n"
  },
  {
    "path": "src/controllers/user.controller.js",
    "content": "const httpStatus = require('http-status');\nconst pick = require('../utils/pick');\nconst ApiError = require('../utils/ApiError');\nconst catchAsync = require('../utils/catchAsync');\nconst { userService } = require('../services');\n\nconst createUser = catchAsync(async (req, res) => {\n  const user = await userService.createUser(req.body);\n  res.status(httpStatus.CREATED).send(user);\n});\n\nconst getUsers = catchAsync(async (req, res) => {\n  const filter = pick(req.query, ['name', 'role']);\n  const options = pick(req.query, ['sortBy', 'limit', 'page']);\n  const result = await userService.queryUsers(filter, options);\n  res.send(result);\n});\n\nconst getUser = catchAsync(async (req, res) => {\n  const user = await userService.getUserById(req.params.userId);\n  if (!user) {\n    throw new ApiError(httpStatus.NOT_FOUND, 'User not found');\n  }\n  res.send(user);\n});\n\nconst updateUser = catchAsync(async (req, res) => {\n  const user = await userService.updateUserById(req.params.userId, req.body);\n  res.send(user);\n});\n\nconst deleteUser = catchAsync(async (req, res) => {\n  await userService.deleteUserById(req.params.userId);\n  res.status(httpStatus.NO_CONTENT).send();\n});\n\nmodule.exports = {\n  createUser,\n  getUsers,\n  getUser,\n  updateUser,\n  deleteUser,\n};\n"
  },
  {
    "path": "src/docs/components.yml",
    "content": "components:\n  schemas:\n    User:\n      type: object\n      properties:\n        id:\n          type: string\n        email:\n          type: string\n          format: email\n        name:\n          type: string\n        role:\n          type: string\n          enum: [user, admin]\n      example:\n        id: 5ebac534954b54139806c112\n        email: fake@example.com\n        name: fake name\n        role: user\n\n    Token:\n      type: object\n      properties:\n        token:\n          type: string\n        expires:\n          type: string\n          format: date-time\n      example:\n        token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg\n        expires: 2020-05-12T16:18:04.793Z\n\n    AuthTokens:\n      type: object\n      properties:\n        access:\n          $ref: '#/components/schemas/Token'\n        refresh:\n          $ref: '#/components/schemas/Token'\n\n    Error:\n      type: object\n      properties:\n        code:\n          type: number\n        message:\n          type: string\n\n  responses:\n    DuplicateEmail:\n      description: Email already taken\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/Error'\n          example:\n            code: 400\n            message: Email already taken\n    Unauthorized:\n      description: Unauthorized\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/Error'\n          example:\n            code: 401\n            message: Please authenticate\n    Forbidden:\n      description: Forbidden\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/Error'\n          example:\n            code: 403\n            message: Forbidden\n    NotFound:\n      description: Not found\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/Error'\n          example:\n            code: 404\n            message: Not found\n\n  securitySchemes:\n    bearerAuth:\n      type: http\n      scheme: bearer\n      bearerFormat: JWT\n"
  },
  {
    "path": "src/docs/swaggerDef.js",
    "content": "const { version } = require('../../package.json');\nconst config = require('../config/config');\n\nconst swaggerDef = {\n  openapi: '3.0.0',\n  info: {\n    title: 'node-express-boilerplate API documentation',\n    version,\n    license: {\n      name: 'MIT',\n      url: 'https://github.com/hagopj13/node-express-boilerplate/blob/master/LICENSE',\n    },\n  },\n  servers: [\n    {\n      url: `http://localhost:${config.port}/v1`,\n    },\n  ],\n};\n\nmodule.exports = swaggerDef;\n"
  },
  {
    "path": "src/index.js",
    "content": "const mongoose = require('mongoose');\nconst app = require('./app');\nconst config = require('./config/config');\nconst logger = require('./config/logger');\n\nlet server;\nmongoose.connect(config.mongoose.url, config.mongoose.options).then(() => {\n  logger.info('Connected to MongoDB');\n  server = app.listen(config.port, () => {\n    logger.info(`Listening to port ${config.port}`);\n  });\n});\n\nconst exitHandler = () => {\n  if (server) {\n    server.close(() => {\n      logger.info('Server closed');\n      process.exit(1);\n    });\n  } else {\n    process.exit(1);\n  }\n};\n\nconst unexpectedErrorHandler = (error) => {\n  logger.error(error);\n  exitHandler();\n};\n\nprocess.on('uncaughtException', unexpectedErrorHandler);\nprocess.on('unhandledRejection', unexpectedErrorHandler);\n\nprocess.on('SIGTERM', () => {\n  logger.info('SIGTERM received');\n  if (server) {\n    server.close();\n  }\n});\n"
  },
  {
    "path": "src/middlewares/auth.js",
    "content": "const passport = require('passport');\nconst httpStatus = require('http-status');\nconst ApiError = require('../utils/ApiError');\nconst { roleRights } = require('../config/roles');\n\nconst verifyCallback = (req, resolve, reject, requiredRights) => async (err, user, info) => {\n  if (err || info || !user) {\n    return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));\n  }\n  req.user = user;\n\n  if (requiredRights.length) {\n    const userRights = roleRights.get(user.role);\n    const hasRequiredRights = requiredRights.every((requiredRight) => userRights.includes(requiredRight));\n    if (!hasRequiredRights && req.params.userId !== user.id) {\n      return reject(new ApiError(httpStatus.FORBIDDEN, 'Forbidden'));\n    }\n  }\n\n  resolve();\n};\n\nconst auth = (...requiredRights) => async (req, res, next) => {\n  return new Promise((resolve, reject) => {\n    passport.authenticate('jwt', { session: false }, verifyCallback(req, resolve, reject, requiredRights))(req, res, next);\n  })\n    .then(() => next())\n    .catch((err) => next(err));\n};\n\nmodule.exports = auth;\n"
  },
  {
    "path": "src/middlewares/error.js",
    "content": "const mongoose = require('mongoose');\nconst httpStatus = require('http-status');\nconst config = require('../config/config');\nconst logger = require('../config/logger');\nconst ApiError = require('../utils/ApiError');\n\nconst errorConverter = (err, req, res, next) => {\n  let error = err;\n  if (!(error instanceof ApiError)) {\n    const statusCode =\n      error.statusCode || error instanceof mongoose.Error ? httpStatus.BAD_REQUEST : httpStatus.INTERNAL_SERVER_ERROR;\n    const message = error.message || httpStatus[statusCode];\n    error = new ApiError(statusCode, message, false, err.stack);\n  }\n  next(error);\n};\n\n// eslint-disable-next-line no-unused-vars\nconst errorHandler = (err, req, res, next) => {\n  let { statusCode, message } = err;\n  if (config.env === 'production' && !err.isOperational) {\n    statusCode = httpStatus.INTERNAL_SERVER_ERROR;\n    message = httpStatus[httpStatus.INTERNAL_SERVER_ERROR];\n  }\n\n  res.locals.errorMessage = err.message;\n\n  const response = {\n    code: statusCode,\n    message,\n    ...(config.env === 'development' && { stack: err.stack }),\n  };\n\n  if (config.env === 'development') {\n    logger.error(err);\n  }\n\n  res.status(statusCode).send(response);\n};\n\nmodule.exports = {\n  errorConverter,\n  errorHandler,\n};\n"
  },
  {
    "path": "src/middlewares/rateLimiter.js",
    "content": "const rateLimit = require('express-rate-limit');\n\nconst authLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000,\n  max: 20,\n  skipSuccessfulRequests: true,\n});\n\nmodule.exports = {\n  authLimiter,\n};\n"
  },
  {
    "path": "src/middlewares/validate.js",
    "content": "const Joi = require('joi');\nconst httpStatus = require('http-status');\nconst pick = require('../utils/pick');\nconst ApiError = require('../utils/ApiError');\n\nconst validate = (schema) => (req, res, next) => {\n  const validSchema = pick(schema, ['params', 'query', 'body']);\n  const object = pick(req, Object.keys(validSchema));\n  const { value, error } = Joi.compile(validSchema)\n    .prefs({ errors: { label: 'key' }, abortEarly: false })\n    .validate(object);\n\n  if (error) {\n    const errorMessage = error.details.map((details) => details.message).join(', ');\n    return next(new ApiError(httpStatus.BAD_REQUEST, errorMessage));\n  }\n  Object.assign(req, value);\n  return next();\n};\n\nmodule.exports = validate;\n"
  },
  {
    "path": "src/models/index.js",
    "content": "module.exports.Token = require('./token.model');\nmodule.exports.User = require('./user.model');\n"
  },
  {
    "path": "src/models/plugins/index.js",
    "content": "module.exports.toJSON = require('./toJSON.plugin');\nmodule.exports.paginate = require('./paginate.plugin');\n"
  },
  {
    "path": "src/models/plugins/paginate.plugin.js",
    "content": "/* eslint-disable no-param-reassign */\n\nconst paginate = (schema) => {\n  /**\n   * @typedef {Object} QueryResult\n   * @property {Document[]} results - Results found\n   * @property {number} page - Current page\n   * @property {number} limit - Maximum number of results per page\n   * @property {number} totalPages - Total number of pages\n   * @property {number} totalResults - Total number of documents\n   */\n  /**\n   * Query for documents with pagination\n   * @param {Object} [filter] - Mongo filter\n   * @param {Object} [options] - Query options\n   * @param {string} [options.sortBy] - Sorting criteria using the format: sortField:(desc|asc). Multiple sorting criteria should be separated by commas (,)\n   * @param {string} [options.populate] - Populate data fields. Hierarchy of fields should be separated by (.). Multiple populating criteria should be separated by commas (,)\n   * @param {number} [options.limit] - Maximum number of results per page (default = 10)\n   * @param {number} [options.page] - Current page (default = 1)\n   * @returns {Promise<QueryResult>}\n   */\n  schema.statics.paginate = async function (filter, options) {\n    let sort = '';\n    if (options.sortBy) {\n      const sortingCriteria = [];\n      options.sortBy.split(',').forEach((sortOption) => {\n        const [key, order] = sortOption.split(':');\n        sortingCriteria.push((order === 'desc' ? '-' : '') + key);\n      });\n      sort = sortingCriteria.join(' ');\n    } else {\n      sort = 'createdAt';\n    }\n\n    const limit = options.limit && parseInt(options.limit, 10) > 0 ? parseInt(options.limit, 10) : 10;\n    const page = options.page && parseInt(options.page, 10) > 0 ? parseInt(options.page, 10) : 1;\n    const skip = (page - 1) * limit;\n\n    const countPromise = this.countDocuments(filter).exec();\n    let docsPromise = this.find(filter).sort(sort).skip(skip).limit(limit);\n\n    if (options.populate) {\n      options.populate.split(',').forEach((populateOption) => {\n        docsPromise = docsPromise.populate(\n          populateOption\n            .split('.')\n            .reverse()\n            .reduce((a, b) => ({ path: b, populate: a }))\n        );\n      });\n    }\n\n    docsPromise = docsPromise.exec();\n\n    return Promise.all([countPromise, docsPromise]).then((values) => {\n      const [totalResults, results] = values;\n      const totalPages = Math.ceil(totalResults / limit);\n      const result = {\n        results,\n        page,\n        limit,\n        totalPages,\n        totalResults,\n      };\n      return Promise.resolve(result);\n    });\n  };\n};\n\nmodule.exports = paginate;\n"
  },
  {
    "path": "src/models/plugins/toJSON.plugin.js",
    "content": "/* eslint-disable no-param-reassign */\n\n/**\n * A mongoose schema plugin which applies the following in the toJSON transform call:\n *  - removes __v, createdAt, updatedAt, and any path that has private: true\n *  - replaces _id with id\n */\n\nconst deleteAtPath = (obj, path, index) => {\n  if (index === path.length - 1) {\n    delete obj[path[index]];\n    return;\n  }\n  deleteAtPath(obj[path[index]], path, index + 1);\n};\n\nconst toJSON = (schema) => {\n  let transform;\n  if (schema.options.toJSON && schema.options.toJSON.transform) {\n    transform = schema.options.toJSON.transform;\n  }\n\n  schema.options.toJSON = Object.assign(schema.options.toJSON || {}, {\n    transform(doc, ret, options) {\n      Object.keys(schema.paths).forEach((path) => {\n        if (schema.paths[path].options && schema.paths[path].options.private) {\n          deleteAtPath(ret, path.split('.'), 0);\n        }\n      });\n\n      ret.id = ret._id.toString();\n      delete ret._id;\n      delete ret.__v;\n      delete ret.createdAt;\n      delete ret.updatedAt;\n      if (transform) {\n        return transform(doc, ret, options);\n      }\n    },\n  });\n};\n\nmodule.exports = toJSON;\n"
  },
  {
    "path": "src/models/token.model.js",
    "content": "const mongoose = require('mongoose');\nconst { toJSON } = require('./plugins');\nconst { tokenTypes } = require('../config/tokens');\n\nconst tokenSchema = mongoose.Schema(\n  {\n    token: {\n      type: String,\n      required: true,\n      index: true,\n    },\n    user: {\n      type: mongoose.SchemaTypes.ObjectId,\n      ref: 'User',\n      required: true,\n    },\n    type: {\n      type: String,\n      enum: [tokenTypes.REFRESH, tokenTypes.RESET_PASSWORD, tokenTypes.VERIFY_EMAIL],\n      required: true,\n    },\n    expires: {\n      type: Date,\n      required: true,\n    },\n    blacklisted: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  {\n    timestamps: true,\n  }\n);\n\n// add plugin that converts mongoose to json\ntokenSchema.plugin(toJSON);\n\n/**\n * @typedef Token\n */\nconst Token = mongoose.model('Token', tokenSchema);\n\nmodule.exports = Token;\n"
  },
  {
    "path": "src/models/user.model.js",
    "content": "const mongoose = require('mongoose');\nconst validator = require('validator');\nconst bcrypt = require('bcryptjs');\nconst { toJSON, paginate } = require('./plugins');\nconst { roles } = require('../config/roles');\n\nconst userSchema = mongoose.Schema(\n  {\n    name: {\n      type: String,\n      required: true,\n      trim: true,\n    },\n    email: {\n      type: String,\n      required: true,\n      unique: true,\n      trim: true,\n      lowercase: true,\n      validate(value) {\n        if (!validator.isEmail(value)) {\n          throw new Error('Invalid email');\n        }\n      },\n    },\n    password: {\n      type: String,\n      required: true,\n      trim: true,\n      minlength: 8,\n      validate(value) {\n        if (!value.match(/\\d/) || !value.match(/[a-zA-Z]/)) {\n          throw new Error('Password must contain at least one letter and one number');\n        }\n      },\n      private: true, // used by the toJSON plugin\n    },\n    role: {\n      type: String,\n      enum: roles,\n      default: 'user',\n    },\n    isEmailVerified: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  {\n    timestamps: true,\n  }\n);\n\n// add plugin that converts mongoose to json\nuserSchema.plugin(toJSON);\nuserSchema.plugin(paginate);\n\n/**\n * Check if email is taken\n * @param {string} email - The user's email\n * @param {ObjectId} [excludeUserId] - The id of the user to be excluded\n * @returns {Promise<boolean>}\n */\nuserSchema.statics.isEmailTaken = async function (email, excludeUserId) {\n  const user = await this.findOne({ email, _id: { $ne: excludeUserId } });\n  return !!user;\n};\n\n/**\n * Check if password matches the user's password\n * @param {string} password\n * @returns {Promise<boolean>}\n */\nuserSchema.methods.isPasswordMatch = async function (password) {\n  const user = this;\n  return bcrypt.compare(password, user.password);\n};\n\nuserSchema.pre('save', async function (next) {\n  const user = this;\n  if (user.isModified('password')) {\n    user.password = await bcrypt.hash(user.password, 8);\n  }\n  next();\n});\n\n/**\n * @typedef User\n */\nconst User = mongoose.model('User', userSchema);\n\nmodule.exports = User;\n"
  },
  {
    "path": "src/routes/v1/auth.route.js",
    "content": "const express = require('express');\nconst validate = require('../../middlewares/validate');\nconst authValidation = require('../../validations/auth.validation');\nconst authController = require('../../controllers/auth.controller');\nconst auth = require('../../middlewares/auth');\n\nconst router = express.Router();\n\nrouter.post('/register', validate(authValidation.register), authController.register);\nrouter.post('/login', validate(authValidation.login), authController.login);\nrouter.post('/logout', validate(authValidation.logout), authController.logout);\nrouter.post('/refresh-tokens', validate(authValidation.refreshTokens), authController.refreshTokens);\nrouter.post('/forgot-password', validate(authValidation.forgotPassword), authController.forgotPassword);\nrouter.post('/reset-password', validate(authValidation.resetPassword), authController.resetPassword);\nrouter.post('/send-verification-email', auth(), authController.sendVerificationEmail);\nrouter.post('/verify-email', validate(authValidation.verifyEmail), authController.verifyEmail);\n\nmodule.exports = router;\n\n/**\n * @swagger\n * tags:\n *   name: Auth\n *   description: Authentication\n */\n\n/**\n * @swagger\n * /auth/register:\n *   post:\n *     summary: Register as user\n *     tags: [Auth]\n *     requestBody:\n *       required: true\n *       content:\n *         application/json:\n *           schema:\n *             type: object\n *             required:\n *               - name\n *               - email\n *               - password\n *             properties:\n *               name:\n *                 type: string\n *               email:\n *                 type: string\n *                 format: email\n *                 description: must be unique\n *               password:\n *                 type: string\n *                 format: password\n *                 minLength: 8\n *                 description: At least one number and one letter\n *             example:\n *               name: fake name\n *               email: fake@example.com\n *               password: password1\n *     responses:\n *       \"201\":\n *         description: Created\n *         content:\n *           application/json:\n *             schema:\n *               type: object\n *               properties:\n *                 user:\n *                   $ref: '#/components/schemas/User'\n *                 tokens:\n *                   $ref: '#/components/schemas/AuthTokens'\n *       \"400\":\n *         $ref: '#/components/responses/DuplicateEmail'\n */\n\n/**\n * @swagger\n * /auth/login:\n *   post:\n *     summary: Login\n *     tags: [Auth]\n *     requestBody:\n *       required: true\n *       content:\n *         application/json:\n *           schema:\n *             type: object\n *             required:\n *               - email\n *               - password\n *             properties:\n *               email:\n *                 type: string\n *                 format: email\n *               password:\n *                 type: string\n *                 format: password\n *             example:\n *               email: fake@example.com\n *               password: password1\n *     responses:\n *       \"200\":\n *         description: OK\n *         content:\n *           application/json:\n *             schema:\n *               type: object\n *               properties:\n *                 user:\n *                   $ref: '#/components/schemas/User'\n *                 tokens:\n *                   $ref: '#/components/schemas/AuthTokens'\n *       \"401\":\n *         description: Invalid email or password\n *         content:\n *           application/json:\n *             schema:\n *               $ref: '#/components/schemas/Error'\n *             example:\n *               code: 401\n *               message: Invalid email or password\n */\n\n/**\n * @swagger\n * /auth/logout:\n *   post:\n *     summary: Logout\n *     tags: [Auth]\n *     requestBody:\n *       required: true\n *       content:\n *         application/json:\n *           schema:\n *             type: object\n *             required:\n *               - refreshToken\n *             properties:\n *               refreshToken:\n *                 type: string\n *             example:\n *               refreshToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg\n *     responses:\n *       \"204\":\n *         description: No content\n *       \"404\":\n *         $ref: '#/components/responses/NotFound'\n */\n\n/**\n * @swagger\n * /auth/refresh-tokens:\n *   post:\n *     summary: Refresh auth tokens\n *     tags: [Auth]\n *     requestBody:\n *       required: true\n *       content:\n *         application/json:\n *           schema:\n *             type: object\n *             required:\n *               - refreshToken\n *             properties:\n *               refreshToken:\n *                 type: string\n *             example:\n *               refreshToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg\n *     responses:\n *       \"200\":\n *         description: OK\n *         content:\n *           application/json:\n *             schema:\n *               $ref: '#/components/schemas/AuthTokens'\n *       \"401\":\n *         $ref: '#/components/responses/Unauthorized'\n */\n\n/**\n * @swagger\n * /auth/forgot-password:\n *   post:\n *     summary: Forgot password\n *     description: An email will be sent to reset password.\n *     tags: [Auth]\n *     requestBody:\n *       required: true\n *       content:\n *         application/json:\n *           schema:\n *             type: object\n *             required:\n *               - email\n *             properties:\n *               email:\n *                 type: string\n *                 format: email\n *             example:\n *               email: fake@example.com\n *     responses:\n *       \"204\":\n *         description: No content\n *       \"404\":\n *         $ref: '#/components/responses/NotFound'\n */\n\n/**\n * @swagger\n * /auth/reset-password:\n *   post:\n *     summary: Reset password\n *     tags: [Auth]\n *     parameters:\n *       - in: query\n *         name: token\n *         required: true\n *         schema:\n *           type: string\n *         description: The reset password token\n *     requestBody:\n *       required: true\n *       content:\n *         application/json:\n *           schema:\n *             type: object\n *             required:\n *               - password\n *             properties:\n *               password:\n *                 type: string\n *                 format: password\n *                 minLength: 8\n *                 description: At least one number and one letter\n *             example:\n *               password: password1\n *     responses:\n *       \"204\":\n *         description: No content\n *       \"401\":\n *         description: Password reset failed\n *         content:\n *           application/json:\n *             schema:\n *               $ref: '#/components/schemas/Error'\n *             example:\n *               code: 401\n *               message: Password reset failed\n */\n\n/**\n * @swagger\n * /auth/send-verification-email:\n *   post:\n *     summary: Send verification email\n *     description: An email will be sent to verify email.\n *     tags: [Auth]\n *     security:\n *       - bearerAuth: []\n *     responses:\n *       \"204\":\n *         description: No content\n *       \"401\":\n *         $ref: '#/components/responses/Unauthorized'\n */\n\n/**\n * @swagger\n * /auth/verify-email:\n *   post:\n *     summary: verify email\n *     tags: [Auth]\n *     parameters:\n *       - in: query\n *         name: token\n *         required: true\n *         schema:\n *           type: string\n *         description: The verify email token\n *     responses:\n *       \"204\":\n *         description: No content\n *       \"401\":\n *         description: verify email failed\n *         content:\n *           application/json:\n *             schema:\n *               $ref: '#/components/schemas/Error'\n *             example:\n *               code: 401\n *               message: verify email failed\n */\n"
  },
  {
    "path": "src/routes/v1/docs.route.js",
    "content": "const express = require('express');\nconst swaggerJsdoc = require('swagger-jsdoc');\nconst swaggerUi = require('swagger-ui-express');\nconst swaggerDefinition = require('../../docs/swaggerDef');\n\nconst router = express.Router();\n\nconst specs = swaggerJsdoc({\n  swaggerDefinition,\n  apis: ['src/docs/*.yml', 'src/routes/v1/*.js'],\n});\n\nrouter.use('/', swaggerUi.serve);\nrouter.get(\n  '/',\n  swaggerUi.setup(specs, {\n    explorer: true,\n  })\n);\n\nmodule.exports = router;\n"
  },
  {
    "path": "src/routes/v1/index.js",
    "content": "const express = require('express');\nconst authRoute = require('./auth.route');\nconst userRoute = require('./user.route');\nconst docsRoute = require('./docs.route');\nconst config = require('../../config/config');\n\nconst router = express.Router();\n\nconst defaultRoutes = [\n  {\n    path: '/auth',\n    route: authRoute,\n  },\n  {\n    path: '/users',\n    route: userRoute,\n  },\n];\n\nconst devRoutes = [\n  // routes available only in development mode\n  {\n    path: '/docs',\n    route: docsRoute,\n  },\n];\n\ndefaultRoutes.forEach((route) => {\n  router.use(route.path, route.route);\n});\n\n/* istanbul ignore next */\nif (config.env === 'development') {\n  devRoutes.forEach((route) => {\n    router.use(route.path, route.route);\n  });\n}\n\nmodule.exports = router;\n"
  },
  {
    "path": "src/routes/v1/user.route.js",
    "content": "const express = require('express');\nconst auth = require('../../middlewares/auth');\nconst validate = require('../../middlewares/validate');\nconst userValidation = require('../../validations/user.validation');\nconst userController = require('../../controllers/user.controller');\n\nconst router = express.Router();\n\nrouter\n  .route('/')\n  .post(auth('manageUsers'), validate(userValidation.createUser), userController.createUser)\n  .get(auth('getUsers'), validate(userValidation.getUsers), userController.getUsers);\n\nrouter\n  .route('/:userId')\n  .get(auth('getUsers'), validate(userValidation.getUser), userController.getUser)\n  .patch(auth('manageUsers'), validate(userValidation.updateUser), userController.updateUser)\n  .delete(auth('manageUsers'), validate(userValidation.deleteUser), userController.deleteUser);\n\nmodule.exports = router;\n\n/**\n * @swagger\n * tags:\n *   name: Users\n *   description: User management and retrieval\n */\n\n/**\n * @swagger\n * /users:\n *   post:\n *     summary: Create a user\n *     description: Only admins can create other users.\n *     tags: [Users]\n *     security:\n *       - bearerAuth: []\n *     requestBody:\n *       required: true\n *       content:\n *         application/json:\n *           schema:\n *             type: object\n *             required:\n *               - name\n *               - email\n *               - password\n *               - role\n *             properties:\n *               name:\n *                 type: string\n *               email:\n *                 type: string\n *                 format: email\n *                 description: must be unique\n *               password:\n *                 type: string\n *                 format: password\n *                 minLength: 8\n *                 description: At least one number and one letter\n *               role:\n *                  type: string\n *                  enum: [user, admin]\n *             example:\n *               name: fake name\n *               email: fake@example.com\n *               password: password1\n *               role: user\n *     responses:\n *       \"201\":\n *         description: Created\n *         content:\n *           application/json:\n *             schema:\n *                $ref: '#/components/schemas/User'\n *       \"400\":\n *         $ref: '#/components/responses/DuplicateEmail'\n *       \"401\":\n *         $ref: '#/components/responses/Unauthorized'\n *       \"403\":\n *         $ref: '#/components/responses/Forbidden'\n *\n *   get:\n *     summary: Get all users\n *     description: Only admins can retrieve all users.\n *     tags: [Users]\n *     security:\n *       - bearerAuth: []\n *     parameters:\n *       - in: query\n *         name: name\n *         schema:\n *           type: string\n *         description: User name\n *       - in: query\n *         name: role\n *         schema:\n *           type: string\n *         description: User role\n *       - in: query\n *         name: sortBy\n *         schema:\n *           type: string\n *         description: sort by query in the form of field:desc/asc (ex. name:asc)\n *       - in: query\n *         name: limit\n *         schema:\n *           type: integer\n *           minimum: 1\n *         default: 10\n *         description: Maximum number of users\n *       - in: query\n *         name: page\n *         schema:\n *           type: integer\n *           minimum: 1\n *           default: 1\n *         description: Page number\n *     responses:\n *       \"200\":\n *         description: OK\n *         content:\n *           application/json:\n *             schema:\n *               type: object\n *               properties:\n *                 results:\n *                   type: array\n *                   items:\n *                     $ref: '#/components/schemas/User'\n *                 page:\n *                   type: integer\n *                   example: 1\n *                 limit:\n *                   type: integer\n *                   example: 10\n *                 totalPages:\n *                   type: integer\n *                   example: 1\n *                 totalResults:\n *                   type: integer\n *                   example: 1\n *       \"401\":\n *         $ref: '#/components/responses/Unauthorized'\n *       \"403\":\n *         $ref: '#/components/responses/Forbidden'\n */\n\n/**\n * @swagger\n * /users/{id}:\n *   get:\n *     summary: Get a user\n *     description: Logged in users can fetch only their own user information. Only admins can fetch other users.\n *     tags: [Users]\n *     security:\n *       - bearerAuth: []\n *     parameters:\n *       - in: path\n *         name: id\n *         required: true\n *         schema:\n *           type: string\n *         description: User id\n *     responses:\n *       \"200\":\n *         description: OK\n *         content:\n *           application/json:\n *             schema:\n *                $ref: '#/components/schemas/User'\n *       \"401\":\n *         $ref: '#/components/responses/Unauthorized'\n *       \"403\":\n *         $ref: '#/components/responses/Forbidden'\n *       \"404\":\n *         $ref: '#/components/responses/NotFound'\n *\n *   patch:\n *     summary: Update a user\n *     description: Logged in users can only update their own information. Only admins can update other users.\n *     tags: [Users]\n *     security:\n *       - bearerAuth: []\n *     parameters:\n *       - in: path\n *         name: id\n *         required: true\n *         schema:\n *           type: string\n *         description: User id\n *     requestBody:\n *       required: true\n *       content:\n *         application/json:\n *           schema:\n *             type: object\n *             properties:\n *               name:\n *                 type: string\n *               email:\n *                 type: string\n *                 format: email\n *                 description: must be unique\n *               password:\n *                 type: string\n *                 format: password\n *                 minLength: 8\n *                 description: At least one number and one letter\n *             example:\n *               name: fake name\n *               email: fake@example.com\n *               password: password1\n *     responses:\n *       \"200\":\n *         description: OK\n *         content:\n *           application/json:\n *             schema:\n *                $ref: '#/components/schemas/User'\n *       \"400\":\n *         $ref: '#/components/responses/DuplicateEmail'\n *       \"401\":\n *         $ref: '#/components/responses/Unauthorized'\n *       \"403\":\n *         $ref: '#/components/responses/Forbidden'\n *       \"404\":\n *         $ref: '#/components/responses/NotFound'\n *\n *   delete:\n *     summary: Delete a user\n *     description: Logged in users can delete only themselves. Only admins can delete other users.\n *     tags: [Users]\n *     security:\n *       - bearerAuth: []\n *     parameters:\n *       - in: path\n *         name: id\n *         required: true\n *         schema:\n *           type: string\n *         description: User id\n *     responses:\n *       \"200\":\n *         description: No content\n *       \"401\":\n *         $ref: '#/components/responses/Unauthorized'\n *       \"403\":\n *         $ref: '#/components/responses/Forbidden'\n *       \"404\":\n *         $ref: '#/components/responses/NotFound'\n */\n"
  },
  {
    "path": "src/services/auth.service.js",
    "content": "const httpStatus = require('http-status');\nconst tokenService = require('./token.service');\nconst userService = require('./user.service');\nconst Token = require('../models/token.model');\nconst ApiError = require('../utils/ApiError');\nconst { tokenTypes } = require('../config/tokens');\n\n/**\n * Login with username and password\n * @param {string} email\n * @param {string} password\n * @returns {Promise<User>}\n */\nconst loginUserWithEmailAndPassword = async (email, password) => {\n  const user = await userService.getUserByEmail(email);\n  if (!user || !(await user.isPasswordMatch(password))) {\n    throw new ApiError(httpStatus.UNAUTHORIZED, 'Incorrect email or password');\n  }\n  return user;\n};\n\n/**\n * Logout\n * @param {string} refreshToken\n * @returns {Promise}\n */\nconst logout = async (refreshToken) => {\n  const refreshTokenDoc = await Token.findOne({ token: refreshToken, type: tokenTypes.REFRESH, blacklisted: false });\n  if (!refreshTokenDoc) {\n    throw new ApiError(httpStatus.NOT_FOUND, 'Not found');\n  }\n  await refreshTokenDoc.remove();\n};\n\n/**\n * Refresh auth tokens\n * @param {string} refreshToken\n * @returns {Promise<Object>}\n */\nconst refreshAuth = async (refreshToken) => {\n  try {\n    const refreshTokenDoc = await tokenService.verifyToken(refreshToken, tokenTypes.REFRESH);\n    const user = await userService.getUserById(refreshTokenDoc.user);\n    if (!user) {\n      throw new Error();\n    }\n    await refreshTokenDoc.remove();\n    return tokenService.generateAuthTokens(user);\n  } catch (error) {\n    throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');\n  }\n};\n\n/**\n * Reset password\n * @param {string} resetPasswordToken\n * @param {string} newPassword\n * @returns {Promise}\n */\nconst resetPassword = async (resetPasswordToken, newPassword) => {\n  try {\n    const resetPasswordTokenDoc = await tokenService.verifyToken(resetPasswordToken, tokenTypes.RESET_PASSWORD);\n    const user = await userService.getUserById(resetPasswordTokenDoc.user);\n    if (!user) {\n      throw new Error();\n    }\n    await userService.updateUserById(user.id, { password: newPassword });\n    await Token.deleteMany({ user: user.id, type: tokenTypes.RESET_PASSWORD });\n  } catch (error) {\n    throw new ApiError(httpStatus.UNAUTHORIZED, 'Password reset failed');\n  }\n};\n\n/**\n * Verify email\n * @param {string} verifyEmailToken\n * @returns {Promise}\n */\nconst verifyEmail = async (verifyEmailToken) => {\n  try {\n    const verifyEmailTokenDoc = await tokenService.verifyToken(verifyEmailToken, tokenTypes.VERIFY_EMAIL);\n    const user = await userService.getUserById(verifyEmailTokenDoc.user);\n    if (!user) {\n      throw new Error();\n    }\n    await Token.deleteMany({ user: user.id, type: tokenTypes.VERIFY_EMAIL });\n    await userService.updateUserById(user.id, { isEmailVerified: true });\n  } catch (error) {\n    throw new ApiError(httpStatus.UNAUTHORIZED, 'Email verification failed');\n  }\n};\n\nmodule.exports = {\n  loginUserWithEmailAndPassword,\n  logout,\n  refreshAuth,\n  resetPassword,\n  verifyEmail,\n};\n"
  },
  {
    "path": "src/services/email.service.js",
    "content": "const nodemailer = require('nodemailer');\nconst config = require('../config/config');\nconst logger = require('../config/logger');\n\nconst transport = nodemailer.createTransport(config.email.smtp);\n/* istanbul ignore next */\nif (config.env !== 'test') {\n  transport\n    .verify()\n    .then(() => logger.info('Connected to email server'))\n    .catch(() => logger.warn('Unable to connect to email server. Make sure you have configured the SMTP options in .env'));\n}\n\n/**\n * Send an email\n * @param {string} to\n * @param {string} subject\n * @param {string} text\n * @returns {Promise}\n */\nconst sendEmail = async (to, subject, text) => {\n  const msg = { from: config.email.from, to, subject, text };\n  await transport.sendMail(msg);\n};\n\n/**\n * Send reset password email\n * @param {string} to\n * @param {string} token\n * @returns {Promise}\n */\nconst sendResetPasswordEmail = async (to, token) => {\n  const subject = 'Reset password';\n  // replace this url with the link to the reset password page of your front-end app\n  const resetPasswordUrl = `http://link-to-app/reset-password?token=${token}`;\n  const text = `Dear user,\nTo reset your password, click on this link: ${resetPasswordUrl}\nIf you did not request any password resets, then ignore this email.`;\n  await sendEmail(to, subject, text);\n};\n\n/**\n * Send verification email\n * @param {string} to\n * @param {string} token\n * @returns {Promise}\n */\nconst sendVerificationEmail = async (to, token) => {\n  const subject = 'Email Verification';\n  // replace this url with the link to the email verification page of your front-end app\n  const verificationEmailUrl = `http://link-to-app/verify-email?token=${token}`;\n  const text = `Dear user,\nTo verify your email, click on this link: ${verificationEmailUrl}\nIf you did not create an account, then ignore this email.`;\n  await sendEmail(to, subject, text);\n};\n\nmodule.exports = {\n  transport,\n  sendEmail,\n  sendResetPasswordEmail,\n  sendVerificationEmail,\n};\n"
  },
  {
    "path": "src/services/index.js",
    "content": "module.exports.authService = require('./auth.service');\nmodule.exports.emailService = require('./email.service');\nmodule.exports.tokenService = require('./token.service');\nmodule.exports.userService = require('./user.service');\n"
  },
  {
    "path": "src/services/token.service.js",
    "content": "const jwt = require('jsonwebtoken');\nconst moment = require('moment');\nconst httpStatus = require('http-status');\nconst config = require('../config/config');\nconst userService = require('./user.service');\nconst { Token } = require('../models');\nconst ApiError = require('../utils/ApiError');\nconst { tokenTypes } = require('../config/tokens');\n\n/**\n * Generate token\n * @param {ObjectId} userId\n * @param {Moment} expires\n * @param {string} type\n * @param {string} [secret]\n * @returns {string}\n */\nconst generateToken = (userId, expires, type, secret = config.jwt.secret) => {\n  const payload = {\n    sub: userId,\n    iat: moment().unix(),\n    exp: expires.unix(),\n    type,\n  };\n  return jwt.sign(payload, secret);\n};\n\n/**\n * Save a token\n * @param {string} token\n * @param {ObjectId} userId\n * @param {Moment} expires\n * @param {string} type\n * @param {boolean} [blacklisted]\n * @returns {Promise<Token>}\n */\nconst saveToken = async (token, userId, expires, type, blacklisted = false) => {\n  const tokenDoc = await Token.create({\n    token,\n    user: userId,\n    expires: expires.toDate(),\n    type,\n    blacklisted,\n  });\n  return tokenDoc;\n};\n\n/**\n * Verify token and return token doc (or throw an error if it is not valid)\n * @param {string} token\n * @param {string} type\n * @returns {Promise<Token>}\n */\nconst verifyToken = async (token, type) => {\n  const payload = jwt.verify(token, config.jwt.secret);\n  const tokenDoc = await Token.findOne({ token, type, user: payload.sub, blacklisted: false });\n  if (!tokenDoc) {\n    throw new Error('Token not found');\n  }\n  return tokenDoc;\n};\n\n/**\n * Generate auth tokens\n * @param {User} user\n * @returns {Promise<Object>}\n */\nconst generateAuthTokens = async (user) => {\n  const accessTokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');\n  const accessToken = generateToken(user.id, accessTokenExpires, tokenTypes.ACCESS);\n\n  const refreshTokenExpires = moment().add(config.jwt.refreshExpirationDays, 'days');\n  const refreshToken = generateToken(user.id, refreshTokenExpires, tokenTypes.REFRESH);\n  await saveToken(refreshToken, user.id, refreshTokenExpires, tokenTypes.REFRESH);\n\n  return {\n    access: {\n      token: accessToken,\n      expires: accessTokenExpires.toDate(),\n    },\n    refresh: {\n      token: refreshToken,\n      expires: refreshTokenExpires.toDate(),\n    },\n  };\n};\n\n/**\n * Generate reset password token\n * @param {string} email\n * @returns {Promise<string>}\n */\nconst generateResetPasswordToken = async (email) => {\n  const user = await userService.getUserByEmail(email);\n  if (!user) {\n    throw new ApiError(httpStatus.NOT_FOUND, 'No users found with this email');\n  }\n  const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');\n  const resetPasswordToken = generateToken(user.id, expires, tokenTypes.RESET_PASSWORD);\n  await saveToken(resetPasswordToken, user.id, expires, tokenTypes.RESET_PASSWORD);\n  return resetPasswordToken;\n};\n\n/**\n * Generate verify email token\n * @param {User} user\n * @returns {Promise<string>}\n */\nconst generateVerifyEmailToken = async (user) => {\n  const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');\n  const verifyEmailToken = generateToken(user.id, expires, tokenTypes.VERIFY_EMAIL);\n  await saveToken(verifyEmailToken, user.id, expires, tokenTypes.VERIFY_EMAIL);\n  return verifyEmailToken;\n};\n\nmodule.exports = {\n  generateToken,\n  saveToken,\n  verifyToken,\n  generateAuthTokens,\n  generateResetPasswordToken,\n  generateVerifyEmailToken,\n};\n"
  },
  {
    "path": "src/services/user.service.js",
    "content": "const httpStatus = require('http-status');\nconst { User } = require('../models');\nconst ApiError = require('../utils/ApiError');\n\n/**\n * Create a user\n * @param {Object} userBody\n * @returns {Promise<User>}\n */\nconst createUser = async (userBody) => {\n  if (await User.isEmailTaken(userBody.email)) {\n    throw new ApiError(httpStatus.BAD_REQUEST, 'Email already taken');\n  }\n  return User.create(userBody);\n};\n\n/**\n * Query for users\n * @param {Object} filter - Mongo filter\n * @param {Object} options - Query options\n * @param {string} [options.sortBy] - Sort option in the format: sortField:(desc|asc)\n * @param {number} [options.limit] - Maximum number of results per page (default = 10)\n * @param {number} [options.page] - Current page (default = 1)\n * @returns {Promise<QueryResult>}\n */\nconst queryUsers = async (filter, options) => {\n  const users = await User.paginate(filter, options);\n  return users;\n};\n\n/**\n * Get user by id\n * @param {ObjectId} id\n * @returns {Promise<User>}\n */\nconst getUserById = async (id) => {\n  return User.findById(id);\n};\n\n/**\n * Get user by email\n * @param {string} email\n * @returns {Promise<User>}\n */\nconst getUserByEmail = async (email) => {\n  return User.findOne({ email });\n};\n\n/**\n * Update user by id\n * @param {ObjectId} userId\n * @param {Object} updateBody\n * @returns {Promise<User>}\n */\nconst updateUserById = async (userId, updateBody) => {\n  const user = await getUserById(userId);\n  if (!user) {\n    throw new ApiError(httpStatus.NOT_FOUND, 'User not found');\n  }\n  if (updateBody.email && (await User.isEmailTaken(updateBody.email, userId))) {\n    throw new ApiError(httpStatus.BAD_REQUEST, 'Email already taken');\n  }\n  Object.assign(user, updateBody);\n  await user.save();\n  return user;\n};\n\n/**\n * Delete user by id\n * @param {ObjectId} userId\n * @returns {Promise<User>}\n */\nconst deleteUserById = async (userId) => {\n  const user = await getUserById(userId);\n  if (!user) {\n    throw new ApiError(httpStatus.NOT_FOUND, 'User not found');\n  }\n  await user.remove();\n  return user;\n};\n\nmodule.exports = {\n  createUser,\n  queryUsers,\n  getUserById,\n  getUserByEmail,\n  updateUserById,\n  deleteUserById,\n};\n"
  },
  {
    "path": "src/utils/ApiError.js",
    "content": "class ApiError extends Error {\n  constructor(statusCode, message, isOperational = true, stack = '') {\n    super(message);\n    this.statusCode = statusCode;\n    this.isOperational = isOperational;\n    if (stack) {\n      this.stack = stack;\n    } else {\n      Error.captureStackTrace(this, this.constructor);\n    }\n  }\n}\n\nmodule.exports = ApiError;\n"
  },
  {
    "path": "src/utils/catchAsync.js",
    "content": "const catchAsync = (fn) => (req, res, next) => {\n  Promise.resolve(fn(req, res, next)).catch((err) => next(err));\n};\n\nmodule.exports = catchAsync;\n"
  },
  {
    "path": "src/utils/pick.js",
    "content": "/**\n * Create an object composed of the picked object properties\n * @param {Object} object\n * @param {string[]} keys\n * @returns {Object}\n */\nconst pick = (object, keys) => {\n  return keys.reduce((obj, key) => {\n    if (object && Object.prototype.hasOwnProperty.call(object, key)) {\n      // eslint-disable-next-line no-param-reassign\n      obj[key] = object[key];\n    }\n    return obj;\n  }, {});\n};\n\nmodule.exports = pick;\n"
  },
  {
    "path": "src/validations/auth.validation.js",
    "content": "const Joi = require('joi');\nconst { password } = require('./custom.validation');\n\nconst register = {\n  body: Joi.object().keys({\n    email: Joi.string().required().email(),\n    password: Joi.string().required().custom(password),\n    name: Joi.string().required(),\n  }),\n};\n\nconst login = {\n  body: Joi.object().keys({\n    email: Joi.string().required(),\n    password: Joi.string().required(),\n  }),\n};\n\nconst logout = {\n  body: Joi.object().keys({\n    refreshToken: Joi.string().required(),\n  }),\n};\n\nconst refreshTokens = {\n  body: Joi.object().keys({\n    refreshToken: Joi.string().required(),\n  }),\n};\n\nconst forgotPassword = {\n  body: Joi.object().keys({\n    email: Joi.string().email().required(),\n  }),\n};\n\nconst resetPassword = {\n  query: Joi.object().keys({\n    token: Joi.string().required(),\n  }),\n  body: Joi.object().keys({\n    password: Joi.string().required().custom(password),\n  }),\n};\n\nconst verifyEmail = {\n  query: Joi.object().keys({\n    token: Joi.string().required(),\n  }),\n};\n\nmodule.exports = {\n  register,\n  login,\n  logout,\n  refreshTokens,\n  forgotPassword,\n  resetPassword,\n  verifyEmail,\n};\n"
  },
  {
    "path": "src/validations/custom.validation.js",
    "content": "const objectId = (value, helpers) => {\n  if (!value.match(/^[0-9a-fA-F]{24}$/)) {\n    return helpers.message('\"{{#label}}\" must be a valid mongo id');\n  }\n  return value;\n};\n\nconst password = (value, helpers) => {\n  if (value.length < 8) {\n    return helpers.message('password must be at least 8 characters');\n  }\n  if (!value.match(/\\d/) || !value.match(/[a-zA-Z]/)) {\n    return helpers.message('password must contain at least 1 letter and 1 number');\n  }\n  return value;\n};\n\nmodule.exports = {\n  objectId,\n  password,\n};\n"
  },
  {
    "path": "src/validations/index.js",
    "content": "module.exports.authValidation = require('./auth.validation');\nmodule.exports.userValidation = require('./user.validation');\n"
  },
  {
    "path": "src/validations/user.validation.js",
    "content": "const Joi = require('joi');\nconst { password, objectId } = require('./custom.validation');\n\nconst createUser = {\n  body: Joi.object().keys({\n    email: Joi.string().required().email(),\n    password: Joi.string().required().custom(password),\n    name: Joi.string().required(),\n    role: Joi.string().required().valid('user', 'admin'),\n  }),\n};\n\nconst getUsers = {\n  query: Joi.object().keys({\n    name: Joi.string(),\n    role: Joi.string(),\n    sortBy: Joi.string(),\n    limit: Joi.number().integer(),\n    page: Joi.number().integer(),\n  }),\n};\n\nconst getUser = {\n  params: Joi.object().keys({\n    userId: Joi.string().custom(objectId),\n  }),\n};\n\nconst updateUser = {\n  params: Joi.object().keys({\n    userId: Joi.required().custom(objectId),\n  }),\n  body: Joi.object()\n    .keys({\n      email: Joi.string().email(),\n      password: Joi.string().custom(password),\n      name: Joi.string(),\n    })\n    .min(1),\n};\n\nconst deleteUser = {\n  params: Joi.object().keys({\n    userId: Joi.string().custom(objectId),\n  }),\n};\n\nmodule.exports = {\n  createUser,\n  getUsers,\n  getUser,\n  updateUser,\n  deleteUser,\n};\n"
  },
  {
    "path": "tests/fixtures/token.fixture.js",
    "content": "const moment = require('moment');\nconst config = require('../../src/config/config');\nconst { tokenTypes } = require('../../src/config/tokens');\nconst tokenService = require('../../src/services/token.service');\nconst { userOne, admin } = require('./user.fixture');\n\nconst accessTokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');\nconst userOneAccessToken = tokenService.generateToken(userOne._id, accessTokenExpires, tokenTypes.ACCESS);\nconst adminAccessToken = tokenService.generateToken(admin._id, accessTokenExpires, tokenTypes.ACCESS);\n\nmodule.exports = {\n  userOneAccessToken,\n  adminAccessToken,\n};\n"
  },
  {
    "path": "tests/fixtures/user.fixture.js",
    "content": "const mongoose = require('mongoose');\nconst bcrypt = require('bcryptjs');\nconst faker = require('faker');\nconst User = require('../../src/models/user.model');\n\nconst password = 'password1';\nconst salt = bcrypt.genSaltSync(8);\nconst hashedPassword = bcrypt.hashSync(password, salt);\n\nconst userOne = {\n  _id: mongoose.Types.ObjectId(),\n  name: faker.name.findName(),\n  email: faker.internet.email().toLowerCase(),\n  password,\n  role: 'user',\n  isEmailVerified: false,\n};\n\nconst userTwo = {\n  _id: mongoose.Types.ObjectId(),\n  name: faker.name.findName(),\n  email: faker.internet.email().toLowerCase(),\n  password,\n  role: 'user',\n  isEmailVerified: false,\n};\n\nconst admin = {\n  _id: mongoose.Types.ObjectId(),\n  name: faker.name.findName(),\n  email: faker.internet.email().toLowerCase(),\n  password,\n  role: 'admin',\n  isEmailVerified: false,\n};\n\nconst insertUsers = async (users) => {\n  await User.insertMany(users.map((user) => ({ ...user, password: hashedPassword })));\n};\n\nmodule.exports = {\n  userOne,\n  userTwo,\n  admin,\n  insertUsers,\n};\n"
  },
  {
    "path": "tests/integration/auth.test.js",
    "content": "const request = require('supertest');\nconst faker = require('faker');\nconst httpStatus = require('http-status');\nconst httpMocks = require('node-mocks-http');\nconst moment = require('moment');\nconst bcrypt = require('bcryptjs');\nconst app = require('../../src/app');\nconst config = require('../../src/config/config');\nconst auth = require('../../src/middlewares/auth');\nconst { tokenService, emailService } = require('../../src/services');\nconst ApiError = require('../../src/utils/ApiError');\nconst setupTestDB = require('../utils/setupTestDB');\nconst { User, Token } = require('../../src/models');\nconst { roleRights } = require('../../src/config/roles');\nconst { tokenTypes } = require('../../src/config/tokens');\nconst { userOne, admin, insertUsers } = require('../fixtures/user.fixture');\nconst { userOneAccessToken, adminAccessToken } = require('../fixtures/token.fixture');\n\nsetupTestDB();\n\ndescribe('Auth routes', () => {\n  describe('POST /v1/auth/register', () => {\n    let newUser;\n    beforeEach(() => {\n      newUser = {\n        name: faker.name.findName(),\n        email: faker.internet.email().toLowerCase(),\n        password: 'password1',\n      };\n    });\n\n    test('should return 201 and successfully register user if request data is ok', async () => {\n      const res = await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.CREATED);\n\n      expect(res.body.user).not.toHaveProperty('password');\n      expect(res.body.user).toEqual({\n        id: expect.anything(),\n        name: newUser.name,\n        email: newUser.email,\n        role: 'user',\n        isEmailVerified: false,\n      });\n\n      const dbUser = await User.findById(res.body.user.id);\n      expect(dbUser).toBeDefined();\n      expect(dbUser.password).not.toBe(newUser.password);\n      expect(dbUser).toMatchObject({ name: newUser.name, email: newUser.email, role: 'user', isEmailVerified: false });\n\n      expect(res.body.tokens).toEqual({\n        access: { token: expect.anything(), expires: expect.anything() },\n        refresh: { token: expect.anything(), expires: expect.anything() },\n      });\n    });\n\n    test('should return 400 error if email is invalid', async () => {\n      newUser.email = 'invalidEmail';\n\n      await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 400 error if email is already used', async () => {\n      await insertUsers([userOne]);\n      newUser.email = userOne.email;\n\n      await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 400 error if password length is less than 8 characters', async () => {\n      newUser.password = 'passwo1';\n\n      await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 400 error if password does not contain both letters and numbers', async () => {\n      newUser.password = 'password';\n\n      await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);\n\n      newUser.password = '11111111';\n\n      await request(app).post('/v1/auth/register').send(newUser).expect(httpStatus.BAD_REQUEST);\n    });\n  });\n\n  describe('POST /v1/auth/login', () => {\n    test('should return 200 and login user if email and password match', async () => {\n      await insertUsers([userOne]);\n      const loginCredentials = {\n        email: userOne.email,\n        password: userOne.password,\n      };\n\n      const res = await request(app).post('/v1/auth/login').send(loginCredentials).expect(httpStatus.OK);\n\n      expect(res.body.user).toEqual({\n        id: expect.anything(),\n        name: userOne.name,\n        email: userOne.email,\n        role: userOne.role,\n        isEmailVerified: userOne.isEmailVerified,\n      });\n\n      expect(res.body.tokens).toEqual({\n        access: { token: expect.anything(), expires: expect.anything() },\n        refresh: { token: expect.anything(), expires: expect.anything() },\n      });\n    });\n\n    test('should return 401 error if there are no users with that email', async () => {\n      const loginCredentials = {\n        email: userOne.email,\n        password: userOne.password,\n      };\n\n      const res = await request(app).post('/v1/auth/login').send(loginCredentials).expect(httpStatus.UNAUTHORIZED);\n\n      expect(res.body).toEqual({ code: httpStatus.UNAUTHORIZED, message: 'Incorrect email or password' });\n    });\n\n    test('should return 401 error if password is wrong', async () => {\n      await insertUsers([userOne]);\n      const loginCredentials = {\n        email: userOne.email,\n        password: 'wrongPassword1',\n      };\n\n      const res = await request(app).post('/v1/auth/login').send(loginCredentials).expect(httpStatus.UNAUTHORIZED);\n\n      expect(res.body).toEqual({ code: httpStatus.UNAUTHORIZED, message: 'Incorrect email or password' });\n    });\n  });\n\n  describe('POST /v1/auth/logout', () => {\n    test('should return 204 if refresh token is valid', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.refreshExpirationDays, 'days');\n      const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);\n      await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);\n\n      await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NO_CONTENT);\n\n      const dbRefreshTokenDoc = await Token.findOne({ token: refreshToken });\n      expect(dbRefreshTokenDoc).toBe(null);\n    });\n\n    test('should return 400 error if refresh token is missing from request body', async () => {\n      await request(app).post('/v1/auth/logout').send().expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 404 error if refresh token is not found in the database', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.refreshExpirationDays, 'days');\n      const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);\n\n      await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NOT_FOUND);\n    });\n\n    test('should return 404 error if refresh token is blacklisted', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.refreshExpirationDays, 'days');\n      const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);\n      await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH, true);\n\n      await request(app).post('/v1/auth/logout').send({ refreshToken }).expect(httpStatus.NOT_FOUND);\n    });\n  });\n\n  describe('POST /v1/auth/refresh-tokens', () => {\n    test('should return 200 and new auth tokens if refresh token is valid', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.refreshExpirationDays, 'days');\n      const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);\n      await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);\n\n      const res = await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.OK);\n\n      expect(res.body).toEqual({\n        access: { token: expect.anything(), expires: expect.anything() },\n        refresh: { token: expect.anything(), expires: expect.anything() },\n      });\n\n      const dbRefreshTokenDoc = await Token.findOne({ token: res.body.refresh.token });\n      expect(dbRefreshTokenDoc).toMatchObject({ type: tokenTypes.REFRESH, user: userOne._id, blacklisted: false });\n\n      const dbRefreshTokenCount = await Token.countDocuments();\n      expect(dbRefreshTokenCount).toBe(1);\n    });\n\n    test('should return 400 error if refresh token is missing from request body', async () => {\n      await request(app).post('/v1/auth/refresh-tokens').send().expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 401 error if refresh token is signed using an invalid secret', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.refreshExpirationDays, 'days');\n      const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH, 'invalidSecret');\n      await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);\n\n      await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 401 error if refresh token is not found in the database', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.refreshExpirationDays, 'days');\n      const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);\n\n      await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 401 error if refresh token is blacklisted', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.refreshExpirationDays, 'days');\n      const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);\n      await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH, true);\n\n      await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 401 error if refresh token is expired', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().subtract(1, 'minutes');\n      const refreshToken = tokenService.generateToken(userOne._id, expires);\n      await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);\n\n      await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 401 error if user is not found', async () => {\n      const expires = moment().add(config.jwt.refreshExpirationDays, 'days');\n      const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);\n      await tokenService.saveToken(refreshToken, userOne._id, expires, tokenTypes.REFRESH);\n\n      await request(app).post('/v1/auth/refresh-tokens').send({ refreshToken }).expect(httpStatus.UNAUTHORIZED);\n    });\n  });\n\n  describe('POST /v1/auth/forgot-password', () => {\n    beforeEach(() => {\n      jest.spyOn(emailService.transport, 'sendMail').mockResolvedValue();\n    });\n\n    test('should return 204 and send reset password email to the user', async () => {\n      await insertUsers([userOne]);\n      const sendResetPasswordEmailSpy = jest.spyOn(emailService, 'sendResetPasswordEmail');\n\n      await request(app).post('/v1/auth/forgot-password').send({ email: userOne.email }).expect(httpStatus.NO_CONTENT);\n\n      expect(sendResetPasswordEmailSpy).toHaveBeenCalledWith(userOne.email, expect.any(String));\n      const resetPasswordToken = sendResetPasswordEmailSpy.mock.calls[0][1];\n      const dbResetPasswordTokenDoc = await Token.findOne({ token: resetPasswordToken, user: userOne._id });\n      expect(dbResetPasswordTokenDoc).toBeDefined();\n    });\n\n    test('should return 400 if email is missing', async () => {\n      await insertUsers([userOne]);\n\n      await request(app).post('/v1/auth/forgot-password').send().expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 404 if email does not belong to any user', async () => {\n      await request(app).post('/v1/auth/forgot-password').send({ email: userOne.email }).expect(httpStatus.NOT_FOUND);\n    });\n  });\n\n  describe('POST /v1/auth/reset-password', () => {\n    test('should return 204 and reset the password', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');\n      const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);\n      await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);\n\n      await request(app)\n        .post('/v1/auth/reset-password')\n        .query({ token: resetPasswordToken })\n        .send({ password: 'password2' })\n        .expect(httpStatus.NO_CONTENT);\n\n      const dbUser = await User.findById(userOne._id);\n      const isPasswordMatch = await bcrypt.compare('password2', dbUser.password);\n      expect(isPasswordMatch).toBe(true);\n\n      const dbResetPasswordTokenCount = await Token.countDocuments({ user: userOne._id, type: tokenTypes.RESET_PASSWORD });\n      expect(dbResetPasswordTokenCount).toBe(0);\n    });\n\n    test('should return 400 if reset password token is missing', async () => {\n      await insertUsers([userOne]);\n\n      await request(app).post('/v1/auth/reset-password').send({ password: 'password2' }).expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 401 if reset password token is blacklisted', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');\n      const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);\n      await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD, true);\n\n      await request(app)\n        .post('/v1/auth/reset-password')\n        .query({ token: resetPasswordToken })\n        .send({ password: 'password2' })\n        .expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 401 if reset password token is expired', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().subtract(1, 'minutes');\n      const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);\n      await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);\n\n      await request(app)\n        .post('/v1/auth/reset-password')\n        .query({ token: resetPasswordToken })\n        .send({ password: 'password2' })\n        .expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 401 if user is not found', async () => {\n      const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');\n      const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);\n      await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);\n\n      await request(app)\n        .post('/v1/auth/reset-password')\n        .query({ token: resetPasswordToken })\n        .send({ password: 'password2' })\n        .expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 400 if password is missing or invalid', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');\n      const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);\n      await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);\n\n      await request(app).post('/v1/auth/reset-password').query({ token: resetPasswordToken }).expect(httpStatus.BAD_REQUEST);\n\n      await request(app)\n        .post('/v1/auth/reset-password')\n        .query({ token: resetPasswordToken })\n        .send({ password: 'short1' })\n        .expect(httpStatus.BAD_REQUEST);\n\n      await request(app)\n        .post('/v1/auth/reset-password')\n        .query({ token: resetPasswordToken })\n        .send({ password: 'password' })\n        .expect(httpStatus.BAD_REQUEST);\n\n      await request(app)\n        .post('/v1/auth/reset-password')\n        .query({ token: resetPasswordToken })\n        .send({ password: '11111111' })\n        .expect(httpStatus.BAD_REQUEST);\n    });\n  });\n\n  describe('POST /v1/auth/send-verification-email', () => {\n    beforeEach(() => {\n      jest.spyOn(emailService.transport, 'sendMail').mockResolvedValue();\n    });\n\n    test('should return 204 and send verification email to the user', async () => {\n      await insertUsers([userOne]);\n      const sendVerificationEmailSpy = jest.spyOn(emailService, 'sendVerificationEmail');\n\n      await request(app)\n        .post('/v1/auth/send-verification-email')\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .expect(httpStatus.NO_CONTENT);\n\n      expect(sendVerificationEmailSpy).toHaveBeenCalledWith(userOne.email, expect.any(String));\n      const verifyEmailToken = sendVerificationEmailSpy.mock.calls[0][1];\n      const dbVerifyEmailToken = await Token.findOne({ token: verifyEmailToken, user: userOne._id });\n\n      expect(dbVerifyEmailToken).toBeDefined();\n    });\n\n    test('should return 401 error if access token is missing', async () => {\n      await insertUsers([userOne]);\n\n      await request(app).post('/v1/auth/send-verification-email').send().expect(httpStatus.UNAUTHORIZED);\n    });\n  });\n\n  describe('POST /v1/auth/verify-email', () => {\n    test('should return 204 and verify the email', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');\n      const verifyEmailToken = tokenService.generateToken(userOne._id, expires);\n      await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL);\n\n      await request(app)\n        .post('/v1/auth/verify-email')\n        .query({ token: verifyEmailToken })\n        .send()\n        .expect(httpStatus.NO_CONTENT);\n\n      const dbUser = await User.findById(userOne._id);\n\n      expect(dbUser.isEmailVerified).toBe(true);\n\n      const dbVerifyEmailToken = await Token.countDocuments({\n        user: userOne._id,\n        type: tokenTypes.VERIFY_EMAIL,\n      });\n      expect(dbVerifyEmailToken).toBe(0);\n    });\n\n    test('should return 400 if verify email token is missing', async () => {\n      await insertUsers([userOne]);\n\n      await request(app).post('/v1/auth/verify-email').send().expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 401 if verify email token is blacklisted', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');\n      const verifyEmailToken = tokenService.generateToken(userOne._id, expires);\n      await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL, true);\n\n      await request(app)\n        .post('/v1/auth/verify-email')\n        .query({ token: verifyEmailToken })\n        .send()\n        .expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 401 if verify email token is expired', async () => {\n      await insertUsers([userOne]);\n      const expires = moment().subtract(1, 'minutes');\n      const verifyEmailToken = tokenService.generateToken(userOne._id, expires);\n      await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL);\n\n      await request(app)\n        .post('/v1/auth/verify-email')\n        .query({ token: verifyEmailToken })\n        .send()\n        .expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 401 if user is not found', async () => {\n      const expires = moment().add(config.jwt.verifyEmailExpirationMinutes, 'minutes');\n      const verifyEmailToken = tokenService.generateToken(userOne._id, expires);\n      await tokenService.saveToken(verifyEmailToken, userOne._id, expires, tokenTypes.VERIFY_EMAIL);\n\n      await request(app)\n        .post('/v1/auth/verify-email')\n        .query({ token: verifyEmailToken })\n        .send()\n        .expect(httpStatus.UNAUTHORIZED);\n    });\n  });\n});\n\ndescribe('Auth middleware', () => {\n  test('should call next with no errors if access token is valid', async () => {\n    await insertUsers([userOne]);\n    const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${userOneAccessToken}` } });\n    const next = jest.fn();\n\n    await auth()(req, httpMocks.createResponse(), next);\n\n    expect(next).toHaveBeenCalledWith();\n    expect(req.user._id).toEqual(userOne._id);\n  });\n\n  test('should call next with unauthorized error if access token is not found in header', async () => {\n    await insertUsers([userOne]);\n    const req = httpMocks.createRequest();\n    const next = jest.fn();\n\n    await auth()(req, httpMocks.createResponse(), next);\n\n    expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n    expect(next).toHaveBeenCalledWith(\n      expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })\n    );\n  });\n\n  test('should call next with unauthorized error if access token is not a valid jwt token', async () => {\n    await insertUsers([userOne]);\n    const req = httpMocks.createRequest({ headers: { Authorization: 'Bearer randomToken' } });\n    const next = jest.fn();\n\n    await auth()(req, httpMocks.createResponse(), next);\n\n    expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n    expect(next).toHaveBeenCalledWith(\n      expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })\n    );\n  });\n\n  test('should call next with unauthorized error if the token is not an access token', async () => {\n    await insertUsers([userOne]);\n    const expires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');\n    const refreshToken = tokenService.generateToken(userOne._id, expires, tokenTypes.REFRESH);\n    const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${refreshToken}` } });\n    const next = jest.fn();\n\n    await auth()(req, httpMocks.createResponse(), next);\n\n    expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n    expect(next).toHaveBeenCalledWith(\n      expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })\n    );\n  });\n\n  test('should call next with unauthorized error if access token is generated with an invalid secret', async () => {\n    await insertUsers([userOne]);\n    const expires = moment().add(config.jwt.accessExpirationMinutes, 'minutes');\n    const accessToken = tokenService.generateToken(userOne._id, expires, tokenTypes.ACCESS, 'invalidSecret');\n    const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${accessToken}` } });\n    const next = jest.fn();\n\n    await auth()(req, httpMocks.createResponse(), next);\n\n    expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n    expect(next).toHaveBeenCalledWith(\n      expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })\n    );\n  });\n\n  test('should call next with unauthorized error if access token is expired', async () => {\n    await insertUsers([userOne]);\n    const expires = moment().subtract(1, 'minutes');\n    const accessToken = tokenService.generateToken(userOne._id, expires, tokenTypes.ACCESS);\n    const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${accessToken}` } });\n    const next = jest.fn();\n\n    await auth()(req, httpMocks.createResponse(), next);\n\n    expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n    expect(next).toHaveBeenCalledWith(\n      expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })\n    );\n  });\n\n  test('should call next with unauthorized error if user is not found', async () => {\n    const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${userOneAccessToken}` } });\n    const next = jest.fn();\n\n    await auth()(req, httpMocks.createResponse(), next);\n\n    expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n    expect(next).toHaveBeenCalledWith(\n      expect.objectContaining({ statusCode: httpStatus.UNAUTHORIZED, message: 'Please authenticate' })\n    );\n  });\n\n  test('should call next with forbidden error if user does not have required rights and userId is not in params', async () => {\n    await insertUsers([userOne]);\n    const req = httpMocks.createRequest({ headers: { Authorization: `Bearer ${userOneAccessToken}` } });\n    const next = jest.fn();\n\n    await auth('anyRight')(req, httpMocks.createResponse(), next);\n\n    expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n    expect(next).toHaveBeenCalledWith(expect.objectContaining({ statusCode: httpStatus.FORBIDDEN, message: 'Forbidden' }));\n  });\n\n  test('should call next with no errors if user does not have required rights but userId is in params', async () => {\n    await insertUsers([userOne]);\n    const req = httpMocks.createRequest({\n      headers: { Authorization: `Bearer ${userOneAccessToken}` },\n      params: { userId: userOne._id.toHexString() },\n    });\n    const next = jest.fn();\n\n    await auth('anyRight')(req, httpMocks.createResponse(), next);\n\n    expect(next).toHaveBeenCalledWith();\n  });\n\n  test('should call next with no errors if user has required rights', async () => {\n    await insertUsers([admin]);\n    const req = httpMocks.createRequest({\n      headers: { Authorization: `Bearer ${adminAccessToken}` },\n      params: { userId: userOne._id.toHexString() },\n    });\n    const next = jest.fn();\n\n    await auth(...roleRights.get('admin'))(req, httpMocks.createResponse(), next);\n\n    expect(next).toHaveBeenCalledWith();\n  });\n});\n"
  },
  {
    "path": "tests/integration/docs.test.js",
    "content": "const request = require('supertest');\nconst httpStatus = require('http-status');\nconst app = require('../../src/app');\nconst config = require('../../src/config/config');\n\ndescribe('Auth routes', () => {\n  describe('GET /v1/docs', () => {\n    test('should return 404 when running in production', async () => {\n      config.env = 'production';\n      await request(app).get('/v1/docs').send().expect(httpStatus.NOT_FOUND);\n      config.env = process.env.NODE_ENV;\n    });\n  });\n});\n"
  },
  {
    "path": "tests/integration/user.test.js",
    "content": "const request = require('supertest');\nconst faker = require('faker');\nconst httpStatus = require('http-status');\nconst app = require('../../src/app');\nconst setupTestDB = require('../utils/setupTestDB');\nconst { User } = require('../../src/models');\nconst { userOne, userTwo, admin, insertUsers } = require('../fixtures/user.fixture');\nconst { userOneAccessToken, adminAccessToken } = require('../fixtures/token.fixture');\n\nsetupTestDB();\n\ndescribe('User routes', () => {\n  describe('POST /v1/users', () => {\n    let newUser;\n\n    beforeEach(() => {\n      newUser = {\n        name: faker.name.findName(),\n        email: faker.internet.email().toLowerCase(),\n        password: 'password1',\n        role: 'user',\n      };\n    });\n\n    test('should return 201 and successfully create new user if data is ok', async () => {\n      await insertUsers([admin]);\n\n      const res = await request(app)\n        .post('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send(newUser)\n        .expect(httpStatus.CREATED);\n\n      expect(res.body).not.toHaveProperty('password');\n      expect(res.body).toEqual({\n        id: expect.anything(),\n        name: newUser.name,\n        email: newUser.email,\n        role: newUser.role,\n        isEmailVerified: false,\n      });\n\n      const dbUser = await User.findById(res.body.id);\n      expect(dbUser).toBeDefined();\n      expect(dbUser.password).not.toBe(newUser.password);\n      expect(dbUser).toMatchObject({ name: newUser.name, email: newUser.email, role: newUser.role, isEmailVerified: false });\n    });\n\n    test('should be able to create an admin as well', async () => {\n      await insertUsers([admin]);\n      newUser.role = 'admin';\n\n      const res = await request(app)\n        .post('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send(newUser)\n        .expect(httpStatus.CREATED);\n\n      expect(res.body.role).toBe('admin');\n\n      const dbUser = await User.findById(res.body.id);\n      expect(dbUser.role).toBe('admin');\n    });\n\n    test('should return 401 error if access token is missing', async () => {\n      await request(app).post('/v1/users').send(newUser).expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 403 error if logged in user is not admin', async () => {\n      await insertUsers([userOne]);\n\n      await request(app)\n        .post('/v1/users')\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send(newUser)\n        .expect(httpStatus.FORBIDDEN);\n    });\n\n    test('should return 400 error if email is invalid', async () => {\n      await insertUsers([admin]);\n      newUser.email = 'invalidEmail';\n\n      await request(app)\n        .post('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send(newUser)\n        .expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 400 error if email is already used', async () => {\n      await insertUsers([admin, userOne]);\n      newUser.email = userOne.email;\n\n      await request(app)\n        .post('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send(newUser)\n        .expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 400 error if password length is less than 8 characters', async () => {\n      await insertUsers([admin]);\n      newUser.password = 'passwo1';\n\n      await request(app)\n        .post('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send(newUser)\n        .expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 400 error if password does not contain both letters and numbers', async () => {\n      await insertUsers([admin]);\n      newUser.password = 'password';\n\n      await request(app)\n        .post('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send(newUser)\n        .expect(httpStatus.BAD_REQUEST);\n\n      newUser.password = '1111111';\n\n      await request(app)\n        .post('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send(newUser)\n        .expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 400 error if role is neither user nor admin', async () => {\n      await insertUsers([admin]);\n      newUser.role = 'invalid';\n\n      await request(app)\n        .post('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send(newUser)\n        .expect(httpStatus.BAD_REQUEST);\n    });\n  });\n\n  describe('GET /v1/users', () => {\n    test('should return 200 and apply the default query options', async () => {\n      await insertUsers([userOne, userTwo, admin]);\n\n      const res = await request(app)\n        .get('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send()\n        .expect(httpStatus.OK);\n\n      expect(res.body).toEqual({\n        results: expect.any(Array),\n        page: 1,\n        limit: 10,\n        totalPages: 1,\n        totalResults: 3,\n      });\n      expect(res.body.results).toHaveLength(3);\n      expect(res.body.results[0]).toEqual({\n        id: userOne._id.toHexString(),\n        name: userOne.name,\n        email: userOne.email,\n        role: userOne.role,\n        isEmailVerified: userOne.isEmailVerified,\n      });\n    });\n\n    test('should return 401 if access token is missing', async () => {\n      await insertUsers([userOne, userTwo, admin]);\n\n      await request(app).get('/v1/users').send().expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 403 if a non-admin is trying to access all users', async () => {\n      await insertUsers([userOne, userTwo, admin]);\n\n      await request(app)\n        .get('/v1/users')\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send()\n        .expect(httpStatus.FORBIDDEN);\n    });\n\n    test('should correctly apply filter on name field', async () => {\n      await insertUsers([userOne, userTwo, admin]);\n\n      const res = await request(app)\n        .get('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .query({ name: userOne.name })\n        .send()\n        .expect(httpStatus.OK);\n\n      expect(res.body).toEqual({\n        results: expect.any(Array),\n        page: 1,\n        limit: 10,\n        totalPages: 1,\n        totalResults: 1,\n      });\n      expect(res.body.results).toHaveLength(1);\n      expect(res.body.results[0].id).toBe(userOne._id.toHexString());\n    });\n\n    test('should correctly apply filter on role field', async () => {\n      await insertUsers([userOne, userTwo, admin]);\n\n      const res = await request(app)\n        .get('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .query({ role: 'user' })\n        .send()\n        .expect(httpStatus.OK);\n\n      expect(res.body).toEqual({\n        results: expect.any(Array),\n        page: 1,\n        limit: 10,\n        totalPages: 1,\n        totalResults: 2,\n      });\n      expect(res.body.results).toHaveLength(2);\n      expect(res.body.results[0].id).toBe(userOne._id.toHexString());\n      expect(res.body.results[1].id).toBe(userTwo._id.toHexString());\n    });\n\n    test('should correctly sort the returned array if descending sort param is specified', async () => {\n      await insertUsers([userOne, userTwo, admin]);\n\n      const res = await request(app)\n        .get('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .query({ sortBy: 'role:desc' })\n        .send()\n        .expect(httpStatus.OK);\n\n      expect(res.body).toEqual({\n        results: expect.any(Array),\n        page: 1,\n        limit: 10,\n        totalPages: 1,\n        totalResults: 3,\n      });\n      expect(res.body.results).toHaveLength(3);\n      expect(res.body.results[0].id).toBe(userOne._id.toHexString());\n      expect(res.body.results[1].id).toBe(userTwo._id.toHexString());\n      expect(res.body.results[2].id).toBe(admin._id.toHexString());\n    });\n\n    test('should correctly sort the returned array if ascending sort param is specified', async () => {\n      await insertUsers([userOne, userTwo, admin]);\n\n      const res = await request(app)\n        .get('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .query({ sortBy: 'role:asc' })\n        .send()\n        .expect(httpStatus.OK);\n\n      expect(res.body).toEqual({\n        results: expect.any(Array),\n        page: 1,\n        limit: 10,\n        totalPages: 1,\n        totalResults: 3,\n      });\n      expect(res.body.results).toHaveLength(3);\n      expect(res.body.results[0].id).toBe(admin._id.toHexString());\n      expect(res.body.results[1].id).toBe(userOne._id.toHexString());\n      expect(res.body.results[2].id).toBe(userTwo._id.toHexString());\n    });\n\n    test('should correctly sort the returned array if multiple sorting criteria are specified', async () => {\n      await insertUsers([userOne, userTwo, admin]);\n\n      const res = await request(app)\n        .get('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .query({ sortBy: 'role:desc,name:asc' })\n        .send()\n        .expect(httpStatus.OK);\n\n      expect(res.body).toEqual({\n        results: expect.any(Array),\n        page: 1,\n        limit: 10,\n        totalPages: 1,\n        totalResults: 3,\n      });\n      expect(res.body.results).toHaveLength(3);\n\n      const expectedOrder = [userOne, userTwo, admin].sort((a, b) => {\n        if (a.role < b.role) {\n          return 1;\n        }\n        if (a.role > b.role) {\n          return -1;\n        }\n        return a.name < b.name ? -1 : 1;\n      });\n\n      expectedOrder.forEach((user, index) => {\n        expect(res.body.results[index].id).toBe(user._id.toHexString());\n      });\n    });\n\n    test('should limit returned array if limit param is specified', async () => {\n      await insertUsers([userOne, userTwo, admin]);\n\n      const res = await request(app)\n        .get('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .query({ limit: 2 })\n        .send()\n        .expect(httpStatus.OK);\n\n      expect(res.body).toEqual({\n        results: expect.any(Array),\n        page: 1,\n        limit: 2,\n        totalPages: 2,\n        totalResults: 3,\n      });\n      expect(res.body.results).toHaveLength(2);\n      expect(res.body.results[0].id).toBe(userOne._id.toHexString());\n      expect(res.body.results[1].id).toBe(userTwo._id.toHexString());\n    });\n\n    test('should return the correct page if page and limit params are specified', async () => {\n      await insertUsers([userOne, userTwo, admin]);\n\n      const res = await request(app)\n        .get('/v1/users')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .query({ page: 2, limit: 2 })\n        .send()\n        .expect(httpStatus.OK);\n\n      expect(res.body).toEqual({\n        results: expect.any(Array),\n        page: 2,\n        limit: 2,\n        totalPages: 2,\n        totalResults: 3,\n      });\n      expect(res.body.results).toHaveLength(1);\n      expect(res.body.results[0].id).toBe(admin._id.toHexString());\n    });\n  });\n\n  describe('GET /v1/users/:userId', () => {\n    test('should return 200 and the user object if data is ok', async () => {\n      await insertUsers([userOne]);\n\n      const res = await request(app)\n        .get(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send()\n        .expect(httpStatus.OK);\n\n      expect(res.body).not.toHaveProperty('password');\n      expect(res.body).toEqual({\n        id: userOne._id.toHexString(),\n        email: userOne.email,\n        name: userOne.name,\n        role: userOne.role,\n        isEmailVerified: userOne.isEmailVerified,\n      });\n    });\n\n    test('should return 401 error if access token is missing', async () => {\n      await insertUsers([userOne]);\n\n      await request(app).get(`/v1/users/${userOne._id}`).send().expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 403 error if user is trying to get another user', async () => {\n      await insertUsers([userOne, userTwo]);\n\n      await request(app)\n        .get(`/v1/users/${userTwo._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send()\n        .expect(httpStatus.FORBIDDEN);\n    });\n\n    test('should return 200 and the user object if admin is trying to get another user', async () => {\n      await insertUsers([userOne, admin]);\n\n      await request(app)\n        .get(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send()\n        .expect(httpStatus.OK);\n    });\n\n    test('should return 400 error if userId is not a valid mongo id', async () => {\n      await insertUsers([admin]);\n\n      await request(app)\n        .get('/v1/users/invalidId')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send()\n        .expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 404 error if user is not found', async () => {\n      await insertUsers([admin]);\n\n      await request(app)\n        .get(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send()\n        .expect(httpStatus.NOT_FOUND);\n    });\n  });\n\n  describe('DELETE /v1/users/:userId', () => {\n    test('should return 204 if data is ok', async () => {\n      await insertUsers([userOne]);\n\n      await request(app)\n        .delete(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send()\n        .expect(httpStatus.NO_CONTENT);\n\n      const dbUser = await User.findById(userOne._id);\n      expect(dbUser).toBeNull();\n    });\n\n    test('should return 401 error if access token is missing', async () => {\n      await insertUsers([userOne]);\n\n      await request(app).delete(`/v1/users/${userOne._id}`).send().expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 403 error if user is trying to delete another user', async () => {\n      await insertUsers([userOne, userTwo]);\n\n      await request(app)\n        .delete(`/v1/users/${userTwo._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send()\n        .expect(httpStatus.FORBIDDEN);\n    });\n\n    test('should return 204 if admin is trying to delete another user', async () => {\n      await insertUsers([userOne, admin]);\n\n      await request(app)\n        .delete(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send()\n        .expect(httpStatus.NO_CONTENT);\n    });\n\n    test('should return 400 error if userId is not a valid mongo id', async () => {\n      await insertUsers([admin]);\n\n      await request(app)\n        .delete('/v1/users/invalidId')\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send()\n        .expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 404 error if user already is not found', async () => {\n      await insertUsers([admin]);\n\n      await request(app)\n        .delete(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send()\n        .expect(httpStatus.NOT_FOUND);\n    });\n  });\n\n  describe('PATCH /v1/users/:userId', () => {\n    test('should return 200 and successfully update user if data is ok', async () => {\n      await insertUsers([userOne]);\n      const updateBody = {\n        name: faker.name.findName(),\n        email: faker.internet.email().toLowerCase(),\n        password: 'newPassword1',\n      };\n\n      const res = await request(app)\n        .patch(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send(updateBody)\n        .expect(httpStatus.OK);\n\n      expect(res.body).not.toHaveProperty('password');\n      expect(res.body).toEqual({\n        id: userOne._id.toHexString(),\n        name: updateBody.name,\n        email: updateBody.email,\n        role: 'user',\n        isEmailVerified: false,\n      });\n\n      const dbUser = await User.findById(userOne._id);\n      expect(dbUser).toBeDefined();\n      expect(dbUser.password).not.toBe(updateBody.password);\n      expect(dbUser).toMatchObject({ name: updateBody.name, email: updateBody.email, role: 'user' });\n    });\n\n    test('should return 401 error if access token is missing', async () => {\n      await insertUsers([userOne]);\n      const updateBody = { name: faker.name.findName() };\n\n      await request(app).patch(`/v1/users/${userOne._id}`).send(updateBody).expect(httpStatus.UNAUTHORIZED);\n    });\n\n    test('should return 403 if user is updating another user', async () => {\n      await insertUsers([userOne, userTwo]);\n      const updateBody = { name: faker.name.findName() };\n\n      await request(app)\n        .patch(`/v1/users/${userTwo._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send(updateBody)\n        .expect(httpStatus.FORBIDDEN);\n    });\n\n    test('should return 200 and successfully update user if admin is updating another user', async () => {\n      await insertUsers([userOne, admin]);\n      const updateBody = { name: faker.name.findName() };\n\n      await request(app)\n        .patch(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send(updateBody)\n        .expect(httpStatus.OK);\n    });\n\n    test('should return 404 if admin is updating another user that is not found', async () => {\n      await insertUsers([admin]);\n      const updateBody = { name: faker.name.findName() };\n\n      await request(app)\n        .patch(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send(updateBody)\n        .expect(httpStatus.NOT_FOUND);\n    });\n\n    test('should return 400 error if userId is not a valid mongo id', async () => {\n      await insertUsers([admin]);\n      const updateBody = { name: faker.name.findName() };\n\n      await request(app)\n        .patch(`/v1/users/invalidId`)\n        .set('Authorization', `Bearer ${adminAccessToken}`)\n        .send(updateBody)\n        .expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 400 if email is invalid', async () => {\n      await insertUsers([userOne]);\n      const updateBody = { email: 'invalidEmail' };\n\n      await request(app)\n        .patch(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send(updateBody)\n        .expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 400 if email is already taken', async () => {\n      await insertUsers([userOne, userTwo]);\n      const updateBody = { email: userTwo.email };\n\n      await request(app)\n        .patch(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send(updateBody)\n        .expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should not return 400 if email is my email', async () => {\n      await insertUsers([userOne]);\n      const updateBody = { email: userOne.email };\n\n      await request(app)\n        .patch(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send(updateBody)\n        .expect(httpStatus.OK);\n    });\n\n    test('should return 400 if password length is less than 8 characters', async () => {\n      await insertUsers([userOne]);\n      const updateBody = { password: 'passwo1' };\n\n      await request(app)\n        .patch(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send(updateBody)\n        .expect(httpStatus.BAD_REQUEST);\n    });\n\n    test('should return 400 if password does not contain both letters and numbers', async () => {\n      await insertUsers([userOne]);\n      const updateBody = { password: 'password' };\n\n      await request(app)\n        .patch(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send(updateBody)\n        .expect(httpStatus.BAD_REQUEST);\n\n      updateBody.password = '11111111';\n\n      await request(app)\n        .patch(`/v1/users/${userOne._id}`)\n        .set('Authorization', `Bearer ${userOneAccessToken}`)\n        .send(updateBody)\n        .expect(httpStatus.BAD_REQUEST);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/unit/middlewares/error.test.js",
    "content": "const mongoose = require('mongoose');\nconst httpStatus = require('http-status');\nconst httpMocks = require('node-mocks-http');\nconst { errorConverter, errorHandler } = require('../../../src/middlewares/error');\nconst ApiError = require('../../../src/utils/ApiError');\nconst config = require('../../../src/config/config');\nconst logger = require('../../../src/config/logger');\n\ndescribe('Error middlewares', () => {\n  describe('Error converter', () => {\n    test('should return the same ApiError object it was called with', () => {\n      const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');\n      const next = jest.fn();\n\n      errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);\n\n      expect(next).toHaveBeenCalledWith(error);\n    });\n\n    test('should convert an Error to ApiError and preserve its status and message', () => {\n      const error = new Error('Any error');\n      error.statusCode = httpStatus.BAD_REQUEST;\n      const next = jest.fn();\n\n      errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);\n\n      expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n      expect(next).toHaveBeenCalledWith(\n        expect.objectContaining({\n          statusCode: error.statusCode,\n          message: error.message,\n          isOperational: false,\n        })\n      );\n    });\n\n    test('should convert an Error without status to ApiError with status 500', () => {\n      const error = new Error('Any error');\n      const next = jest.fn();\n\n      errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);\n\n      expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n      expect(next).toHaveBeenCalledWith(\n        expect.objectContaining({\n          statusCode: httpStatus.INTERNAL_SERVER_ERROR,\n          message: error.message,\n          isOperational: false,\n        })\n      );\n    });\n\n    test('should convert an Error without message to ApiError with default message of that http status', () => {\n      const error = new Error();\n      error.statusCode = httpStatus.BAD_REQUEST;\n      const next = jest.fn();\n\n      errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);\n\n      expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n      expect(next).toHaveBeenCalledWith(\n        expect.objectContaining({\n          statusCode: error.statusCode,\n          message: httpStatus[error.statusCode],\n          isOperational: false,\n        })\n      );\n    });\n\n    test('should convert a Mongoose error to ApiError with status 400 and preserve its message', () => {\n      const error = new mongoose.Error('Any mongoose error');\n      const next = jest.fn();\n\n      errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);\n\n      expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n      expect(next).toHaveBeenCalledWith(\n        expect.objectContaining({\n          statusCode: httpStatus.BAD_REQUEST,\n          message: error.message,\n          isOperational: false,\n        })\n      );\n    });\n\n    test('should convert any other object to ApiError with status 500 and its message', () => {\n      const error = {};\n      const next = jest.fn();\n\n      errorConverter(error, httpMocks.createRequest(), httpMocks.createResponse(), next);\n\n      expect(next).toHaveBeenCalledWith(expect.any(ApiError));\n      expect(next).toHaveBeenCalledWith(\n        expect.objectContaining({\n          statusCode: httpStatus.INTERNAL_SERVER_ERROR,\n          message: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],\n          isOperational: false,\n        })\n      );\n    });\n  });\n\n  describe('Error handler', () => {\n    beforeEach(() => {\n      jest.spyOn(logger, 'error').mockImplementation(() => {});\n    });\n\n    test('should send proper error response and put the error message in res.locals', () => {\n      const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');\n      const res = httpMocks.createResponse();\n      const sendSpy = jest.spyOn(res, 'send');\n\n      errorHandler(error, httpMocks.createRequest(), res);\n\n      expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ code: error.statusCode, message: error.message }));\n      expect(res.locals.errorMessage).toBe(error.message);\n    });\n\n    test('should put the error stack in the response if in development mode', () => {\n      config.env = 'development';\n      const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');\n      const res = httpMocks.createResponse();\n      const sendSpy = jest.spyOn(res, 'send');\n\n      errorHandler(error, httpMocks.createRequest(), res);\n\n      expect(sendSpy).toHaveBeenCalledWith(\n        expect.objectContaining({ code: error.statusCode, message: error.message, stack: error.stack })\n      );\n      config.env = process.env.NODE_ENV;\n    });\n\n    test('should send internal server error status and message if in production mode and error is not operational', () => {\n      config.env = 'production';\n      const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error', false);\n      const res = httpMocks.createResponse();\n      const sendSpy = jest.spyOn(res, 'send');\n\n      errorHandler(error, httpMocks.createRequest(), res);\n\n      expect(sendSpy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          code: httpStatus.INTERNAL_SERVER_ERROR,\n          message: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],\n        })\n      );\n      expect(res.locals.errorMessage).toBe(error.message);\n      config.env = process.env.NODE_ENV;\n    });\n\n    test('should preserve original error status and message if in production mode and error is operational', () => {\n      config.env = 'production';\n      const error = new ApiError(httpStatus.BAD_REQUEST, 'Any error');\n      const res = httpMocks.createResponse();\n      const sendSpy = jest.spyOn(res, 'send');\n\n      errorHandler(error, httpMocks.createRequest(), res);\n\n      expect(sendSpy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          code: error.statusCode,\n          message: error.message,\n        })\n      );\n      config.env = process.env.NODE_ENV;\n    });\n  });\n});\n"
  },
  {
    "path": "tests/unit/models/plugins/paginate.plugin.test.js",
    "content": "const mongoose = require('mongoose');\nconst setupTestDB = require('../../../utils/setupTestDB');\nconst paginate = require('../../../../src/models/plugins/paginate.plugin');\n\nconst projectSchema = mongoose.Schema({\n  name: {\n    type: String,\n    required: true,\n  },\n});\n\nprojectSchema.virtual('tasks', {\n  ref: 'Task',\n  localField: '_id',\n  foreignField: 'project',\n});\n\nprojectSchema.plugin(paginate);\nconst Project = mongoose.model('Project', projectSchema);\n\nconst taskSchema = mongoose.Schema({\n  name: {\n    type: String,\n    required: true,\n  },\n  project: {\n    type: mongoose.SchemaTypes.ObjectId,\n    ref: 'Project',\n    required: true,\n  },\n});\n\ntaskSchema.plugin(paginate);\nconst Task = mongoose.model('Task', taskSchema);\n\nsetupTestDB();\n\ndescribe('paginate plugin', () => {\n  describe('populate option', () => {\n    test('should populate the specified data fields', async () => {\n      const project = await Project.create({ name: 'Project One' });\n      const task = await Task.create({ name: 'Task One', project: project._id });\n\n      const taskPages = await Task.paginate({ _id: task._id }, { populate: 'project' });\n\n      expect(taskPages.results[0].project).toHaveProperty('_id', project._id);\n    });\n\n    test('should populate nested fields', async () => {\n      const project = await Project.create({ name: 'Project One' });\n      const task = await Task.create({ name: 'Task One', project: project._id });\n\n      const projectPages = await Project.paginate({ _id: project._id }, { populate: 'tasks.project' });\n      const { tasks } = projectPages.results[0];\n\n      expect(tasks).toHaveLength(1);\n      expect(tasks[0]).toHaveProperty('_id', task._id);\n      expect(tasks[0].project).toHaveProperty('_id', project._id);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/unit/models/plugins/toJSON.plugin.test.js",
    "content": "const mongoose = require('mongoose');\nconst { toJSON } = require('../../../../src/models/plugins');\n\ndescribe('toJSON plugin', () => {\n  let connection;\n\n  beforeEach(() => {\n    connection = mongoose.createConnection();\n  });\n\n  it('should replace _id with id', () => {\n    const schema = mongoose.Schema();\n    schema.plugin(toJSON);\n    const Model = connection.model('Model', schema);\n    const doc = new Model();\n    expect(doc.toJSON()).not.toHaveProperty('_id');\n    expect(doc.toJSON()).toHaveProperty('id', doc._id.toString());\n  });\n\n  it('should remove __v', () => {\n    const schema = mongoose.Schema();\n    schema.plugin(toJSON);\n    const Model = connection.model('Model', schema);\n    const doc = new Model();\n    expect(doc.toJSON()).not.toHaveProperty('__v');\n  });\n\n  it('should remove createdAt and updatedAt', () => {\n    const schema = mongoose.Schema({}, { timestamps: true });\n    schema.plugin(toJSON);\n    const Model = connection.model('Model', schema);\n    const doc = new Model();\n    expect(doc.toJSON()).not.toHaveProperty('createdAt');\n    expect(doc.toJSON()).not.toHaveProperty('updatedAt');\n  });\n\n  it('should remove any path set as private', () => {\n    const schema = mongoose.Schema({\n      public: { type: String },\n      private: { type: String, private: true },\n    });\n    schema.plugin(toJSON);\n    const Model = connection.model('Model', schema);\n    const doc = new Model({ public: 'some public value', private: 'some private value' });\n    expect(doc.toJSON()).not.toHaveProperty('private');\n    expect(doc.toJSON()).toHaveProperty('public');\n  });\n\n  it('should remove any nested paths set as private', () => {\n    const schema = mongoose.Schema({\n      public: { type: String },\n      nested: {\n        private: { type: String, private: true },\n      },\n    });\n    schema.plugin(toJSON);\n    const Model = connection.model('Model', schema);\n    const doc = new Model({\n      public: 'some public value',\n      nested: {\n        private: 'some nested private value',\n      },\n    });\n    expect(doc.toJSON()).not.toHaveProperty('nested.private');\n    expect(doc.toJSON()).toHaveProperty('public');\n  });\n\n  it('should also call the schema toJSON transform function', () => {\n    const schema = mongoose.Schema(\n      {\n        public: { type: String },\n        private: { type: String },\n      },\n      {\n        toJSON: {\n          transform: (doc, ret) => {\n            // eslint-disable-next-line no-param-reassign\n            delete ret.private;\n          },\n        },\n      }\n    );\n    schema.plugin(toJSON);\n    const Model = connection.model('Model', schema);\n    const doc = new Model({ public: 'some public value', private: 'some private value' });\n    expect(doc.toJSON()).not.toHaveProperty('private');\n    expect(doc.toJSON()).toHaveProperty('public');\n  });\n});\n"
  },
  {
    "path": "tests/unit/models/user.model.test.js",
    "content": "const faker = require('faker');\nconst { User } = require('../../../src/models');\n\ndescribe('User model', () => {\n  describe('User validation', () => {\n    let newUser;\n    beforeEach(() => {\n      newUser = {\n        name: faker.name.findName(),\n        email: faker.internet.email().toLowerCase(),\n        password: 'password1',\n        role: 'user',\n      };\n    });\n\n    test('should correctly validate a valid user', async () => {\n      await expect(new User(newUser).validate()).resolves.toBeUndefined();\n    });\n\n    test('should throw a validation error if email is invalid', async () => {\n      newUser.email = 'invalidEmail';\n      await expect(new User(newUser).validate()).rejects.toThrow();\n    });\n\n    test('should throw a validation error if password length is less than 8 characters', async () => {\n      newUser.password = 'passwo1';\n      await expect(new User(newUser).validate()).rejects.toThrow();\n    });\n\n    test('should throw a validation error if password does not contain numbers', async () => {\n      newUser.password = 'password';\n      await expect(new User(newUser).validate()).rejects.toThrow();\n    });\n\n    test('should throw a validation error if password does not contain letters', async () => {\n      newUser.password = '11111111';\n      await expect(new User(newUser).validate()).rejects.toThrow();\n    });\n\n    test('should throw a validation error if role is unknown', async () => {\n      newUser.role = 'invalid';\n      await expect(new User(newUser).validate()).rejects.toThrow();\n    });\n  });\n\n  describe('User toJSON()', () => {\n    test('should not return user password when toJSON is called', () => {\n      const newUser = {\n        name: faker.name.findName(),\n        email: faker.internet.email().toLowerCase(),\n        password: 'password1',\n        role: 'user',\n      };\n      expect(new User(newUser).toJSON()).not.toHaveProperty('password');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/utils/setupTestDB.js",
    "content": "const mongoose = require('mongoose');\nconst config = require('../../src/config/config');\n\nconst setupTestDB = () => {\n  beforeAll(async () => {\n    await mongoose.connect(config.mongoose.url, config.mongoose.options);\n  });\n\n  beforeEach(async () => {\n    await Promise.all(Object.values(mongoose.connection.collections).map(async (collection) => collection.deleteMany()));\n  });\n\n  afterAll(async () => {\n    await mongoose.disconnect();\n  });\n};\n\nmodule.exports = setupTestDB;\n"
  }
]