├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierrc.json ├── .travis.yml ├── .vscode └── launch.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── controllers │ ├── auth │ │ ├── forgotPassword.js │ │ ├── getRefreshToken.js │ │ ├── helpers │ │ │ ├── blockIsExpired.js │ │ │ ├── blockUser.js │ │ │ ├── checkLoginAttemptsAndBlockExpires.js │ │ │ ├── checkPermissions.js │ │ │ ├── findForgotPassword.js │ │ │ ├── findUser.js │ │ │ ├── findUserById.js │ │ │ ├── findUserToResetPassword.js │ │ │ ├── forgotPasswordResponse.js │ │ │ ├── generateToken.js │ │ │ ├── getUserIdFromToken.js │ │ │ ├── index.js │ │ │ ├── markResetPasswordAsUsed.js │ │ │ ├── passwordsDoNotMatch.js │ │ │ ├── registerUser.js │ │ │ ├── returnRegisterToken.js │ │ │ ├── saveForgotPassword.js │ │ │ ├── saveLoginAttemptsToDB.js │ │ │ ├── saveUserAccessAndReturnToken.js │ │ │ ├── setUserInfo.js │ │ │ ├── updatePassword.js │ │ │ ├── userIsBlocked.js │ │ │ ├── verificationExists.js │ │ │ └── verifyUser.js │ │ ├── index.js │ │ ├── login.js │ │ ├── register.js │ │ ├── resetPassword.js │ │ ├── roleAuthorization.js │ │ ├── validators │ │ │ ├── index.js │ │ │ ├── validateForgotPassword.js │ │ │ ├── validateLogin.js │ │ │ ├── validateRegister.js │ │ │ ├── validateResetPassword.js │ │ │ └── validateVerify.js │ │ └── verify.js │ ├── cities │ │ ├── createCity.js │ │ ├── deleteCity.js │ │ ├── getAllCities.js │ │ ├── getCities.js │ │ ├── getCity.js │ │ ├── helpers │ │ │ ├── cityExists.js │ │ │ ├── cityExistsExcludingItself.js │ │ │ ├── getAllItemsFromDB.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── updateCity.js │ │ └── validators │ │ │ ├── index.js │ │ │ ├── validateCreateCity.js │ │ │ ├── validateDeleteCity.js │ │ │ ├── validateGetCity.js │ │ │ └── validateUpdateCity.js │ ├── profile │ │ ├── changePassword.js │ │ ├── getProfile.js │ │ ├── helpers │ │ │ ├── changePasswordInDB.js │ │ │ ├── findUser.js │ │ │ ├── getProfileFromDB.js │ │ │ ├── index.js │ │ │ └── updateProfileInDB.js │ │ ├── index.js │ │ ├── updateProfile.js │ │ └── validators │ │ │ ├── index.js │ │ │ ├── validateChangePassword.js │ │ │ └── validateUpdateProfile.js │ └── users │ │ ├── createUser.js │ │ ├── deleteUser.js │ │ ├── getUser.js │ │ ├── getUsers.js │ │ ├── helpers │ │ ├── createItemInDb.js │ │ └── index.js │ │ ├── index.js │ │ ├── updateUser.js │ │ └── validators │ │ ├── index.js │ │ ├── validateCreateUser.js │ │ ├── validateDeleteUser.js │ │ ├── validateGetUser.js │ │ └── validateUpdateUser.js ├── middleware │ ├── auth │ │ ├── checkPassword.js │ │ ├── decrypt.js │ │ ├── encrypt.js │ │ └── index.js │ ├── db │ │ ├── buildSort.js │ │ ├── checkQueryString.js │ │ ├── cleanPaginationID.js │ │ ├── createItem.js │ │ ├── deleteItem.js │ │ ├── getItem.js │ │ ├── getItems.js │ │ ├── index.js │ │ ├── listInitOptions.js │ │ └── updateItem.js │ ├── emailer │ │ ├── emailExists.js │ │ ├── emailExistsExcludingMyself.js │ │ ├── index.js │ │ ├── prepareToSendEmail.js │ │ ├── sendEmail.js │ │ ├── sendRegistrationEmailMessage.js │ │ └── sendResetPasswordEmailMessage.js │ └── utils │ │ ├── buildErrObject.js │ │ ├── buildSuccObject.js │ │ ├── getBrowserInfo.js │ │ ├── getCountry.js │ │ ├── getIP.js │ │ ├── handleError.js │ │ ├── handleError.test.js │ │ ├── index.js │ │ ├── isIDGood.js │ │ ├── itemNotFound.js │ │ ├── removeExtensionFromFile.js │ │ └── validateResult.js ├── models │ ├── city.js │ ├── forgotPassword.js │ ├── index.js │ ├── user.js │ └── userAccess.js └── routes │ ├── auth.js │ ├── cities.js │ ├── index.js │ ├── profile.js │ └── users.js ├── clean.js ├── config ├── mongo.js └── passport.js ├── data ├── 1.users │ └── user.js └── 2.cities │ └── city.js ├── jest.config.js ├── locales ├── en.json └── es.json ├── package-lock.json ├── package.json ├── posman-example.json ├── public └── .gitkeep ├── seed.js ├── server.js ├── test ├── auth.js ├── cities.js ├── profile.js └── users.js └── views └── index.html /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | JWT_SECRET=MyUltraSecurePassWordIWontForgetToChange 3 | JWT_EXPIRATION_IN_MINUTES=4320 4 | MONGO_URI=mongodb://localhost:27017/myprojectdbname 5 | EMAIL_FROM_NAME=My Project 6 | EMAIL_FROM_ADDRESS=info@myproject.com 7 | EMAIL_SMTP_DOMAIN_MAILGUN=myproject.com 8 | EMAIL_SMTP_API_MAILGUN=123456 9 | FRONTEND_URL=http://localhost:8080 10 | USE_REDIS=false 11 | REDIS_HOST=127.0.0.1 12 | REDIS_PORT=6379 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "env": { 5 | "node": true, 6 | "mocha": true, 7 | "es6": true 8 | }, 9 | 10 | "globals": { 11 | "Promise": true, 12 | "_": true, 13 | "async": true, 14 | "expect": true, 15 | "jest": true 16 | }, 17 | 18 | "rules": { 19 | "callback-return": [ 20 | "error", 21 | ["done", "proceed", "next", "onwards", "callback", "cb"] 22 | ], 23 | "camelcase": [ 24 | "warn", 25 | { 26 | "properties": "always" 27 | } 28 | ], 29 | "comma-style": ["warn", "last"], 30 | "curly": ["error"], 31 | "eqeqeq": ["error", "always"], 32 | "eol-last": ["warn"], 33 | "no-undef": 2, 34 | "handle-callback-err": ["error"], 35 | "arrow-body-style": ["off", 2], 36 | "indent": ["off", 2], 37 | "linebreak-style": ["error", "unix"], 38 | "no-dupe-keys": ["error"], 39 | "no-duplicate-case": ["error"], 40 | "no-extra-semi": ["warn"], 41 | "no-labels": ["error"], 42 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 43 | "no-redeclare": ["warn"], 44 | "no-return-assign": ["error", "always"], 45 | "no-sequences": ["error"], 46 | "no-trailing-spaces": ["warn"], 47 | "no-unexpected-multiline": ["warn"], 48 | "no-unreachable": ["warn"], 49 | "no-magic-numbers": ["off"], 50 | "max-params": ["off"], 51 | "max-len": ["off"], 52 | "max-nested-callbacks": ["off"], 53 | "new-cap": ["off"], 54 | "consistent-this": ["error", "that"], 55 | "no-unused-vars": [ 56 | "error", 57 | { 58 | "caughtErrors": "all", 59 | "caughtErrorsIgnorePattern": "^unused($|[A-Z].*$)" 60 | } 61 | ], 62 | "no-use-before-define": [ 63 | "error", 64 | { 65 | "functions": false 66 | } 67 | ], 68 | "no-var": 2, 69 | "one-var": ["warn", "never"], 70 | "prefer-arrow-callback": [ 71 | "warn", 72 | { 73 | "allowNamedFunctions": true 74 | } 75 | ], 76 | "quotes": [ 77 | "warn", 78 | "single", 79 | { 80 | "avoidEscape": false, 81 | "allowTemplateLiterals": true 82 | } 83 | ], 84 | "semi-spacing": [ 85 | "warn", 86 | { 87 | "before": false, 88 | "after": true 89 | } 90 | ], 91 | "semi-style": ["warn", "last"], 92 | "space-before-function-paren": ["off", 2], 93 | "prettier/prettier": "error" 94 | }, 95 | "extends": [ 96 | "formidable/rules/eslint/best-practices/off", 97 | "formidable/rules/eslint/es6/on", 98 | "formidable/rules/eslint/errors/off", 99 | "formidable/rules/eslint/strict/on", 100 | "formidable/rules/eslint/node/off", 101 | "formidable/rules/eslint/style/on", 102 | "formidable/rules/eslint/variables/on", 103 | "prettier" 104 | ], 105 | "plugins": ["prettier"] 106 | } 107 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Smartphone (please complete the following information):** 24 | - OS: [e.g. macOS High Sierra, Linux, Windows] 25 | - API client: [e.g. postman, cURL] 26 | - Version [e.g. 22] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | 65 | # don't ignore keep files 66 | !.gitkeep 67 | 68 | # ignore production configuration 69 | .env.production 70 | 71 | .history/ 72 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "bracketSpacing": true 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: node_js 3 | node_js: 4 | - '12' 5 | cache: 6 | npm: true 7 | services: 8 | - mongodb 9 | before_script: 10 | - cp .env.example .env 11 | env: 12 | - NODE_ENV=test 13 | if: tag IS present 14 | deploy: 15 | provider: npm 16 | email: $NPM_EMAIL 17 | api_key: $NPM_DEPLOY_TOKEN 18 | on: 19 | branch: master 20 | tags: true 21 | repo: davellanedam/node-express-mongodb-jwt-rest-api-skeleton 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch nodemon in debug", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/nodemon", 9 | "program": "${workspaceFolder}/server.js", 10 | "restart": true, 11 | "protocol": "inspector", 12 | "console": "integratedTerminal", 13 | "internalConsoleOptions": "neverOpen", 14 | "port": 9230 15 | }, 16 | { 17 | "name": "Launch mocha test in debug", 18 | "request": "launch", 19 | "runtimeArgs": ["run", "mocha", "${relativeFile}"], 20 | "runtimeExecutable": "npm", 21 | "skipFiles": ["/**"], 22 | "type": "node", 23 | "env": { 24 | "PORT": "3111" 25 | } 26 | }, 27 | { 28 | "name": "Launch jest in debug", 29 | "type": "node", 30 | "request": "launch", 31 | "cwd": "${workspaceFolder}", 32 | "runtimeArgs": [ 33 | "--inspect-brk", 34 | "node_modules/.bin/jest", 35 | "--runInBand", 36 | "--config=jest.config.js", 37 | "${file}" 38 | ], 39 | "port": 9231 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v9.0.5 (April 30, 2022) 2 | 3 | * NPM updated 4 | 5 | ## v9.0.4 (January 23, 2020) 6 | 7 | * Added extras tests ([#218](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/pull/218)) 8 | 9 | ## v9.0.3 (January 23, 2020) 10 | 11 | * NPM updated 12 | * Fixed test failing in Windows 10 ([#221](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/issues/221)) 13 | * Fixed mailgun in european regions ([#222](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/issues/222)) 14 | * Fixed i18n and helmet in normal dependencies ([#224](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/issues/224)) 15 | 16 | ## v9.0.2 (November 16, 2020) 17 | 18 | * NPM updated 19 | * Fixed typo ([#214](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/pull/214)) 20 | 21 | ## v9.0.1 (October 12, 2020) 22 | 23 | * NPM updated 24 | 25 | ## v9.0.0 (October 3, 2020) 26 | 27 | * Major breaking changes, now controllers and middleware is split in single files for more scalability and testability 28 | * Added Jest setup 29 | * Added first Jest test 30 | * Added VS Code setup for debugging Jest and nodemon 31 | * Added more reports for coverage, now there are three: 1 for Jest tests, 1 for mocha tests (end to end) and a 3rd that merges the previous 2. 32 | * NPM updated 33 | 34 | ## v8.1.5 (July 30, 2020) 35 | 36 | * NPM updated 37 | 38 | ## v8.1.4 (June 21, 2020) 39 | 40 | * lint-staged package added 41 | 42 | ## v8.1.3 (June 21, 2020) 43 | 44 | * Adding additional key to redis ([#200](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/issues/200)) 45 | 46 | ## v8.1.2 (June 21, 2020) 47 | 48 | * NPM updated 49 | 50 | ## v8.1.1 (March 30, 2020) 51 | 52 | * NPM updated 53 | * Prettier updated to 2.x 54 | 55 | ## v8.0.1 (March 16, 2020) 56 | 57 | * NPM updated 58 | 59 | ## v8.0.0 (March 16, 2020) 60 | 61 | * This major version requires node 10+ because new bcrypt lib 62 | * Use of bcrypt lib 63 | * New test for users 64 | * NPM updated 65 | 66 | ## v7.1.2 (January 12, 2020) 67 | 68 | * Added cross-env to solve windows envionment issues 69 | * Use Mongoose built in function to validate ID 70 | * NPM updated 71 | 72 | ## v7.1.1 (September 29, 2019) 73 | 74 | * Added new option MongoClient constructor 75 | * NPM updated 76 | 77 | ## v7.1.0 (July 25, 2019) 78 | 79 | * Postman Collection example now included in root directory. Now /login has a test that automatically gets ans sets token. Thank you Glen! ([#92](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/pull/92)) 80 | * NPM updated 81 | 82 | ## v7.0.2 (July 12, 2019) 83 | 84 | * NPM updated 85 | 86 | ## v7.0.1 (July 10, 2019) 87 | 88 | * NPM updated 89 | 90 | ## v7.0.0 (July 3, 2019) 91 | 92 | * Making express-validator happy 93 | 94 | ## v6.1.20 (July 3, 2019) 95 | 96 | * NPM updated 97 | 98 | ## v6.1.19 (June 12, 2019) 99 | 100 | * NPM updated 101 | 102 | ## v6.1.18 (June 5, 2019) 103 | 104 | * NPM updated 105 | 106 | ## v6.1.17 (June 4, 2019) 107 | 108 | * NPM updated 109 | * Typos 110 | 111 | ## v6.1.16 (May 27, 2019) 112 | 113 | * NPM updated 114 | 115 | ## v6.1.15 (May 23, 2019) 116 | 117 | * NPM updated 118 | 119 | ## v6.1.14 (May 22, 2019) 120 | 121 | * NPM updated 122 | 123 | ## v6.1.13 (May 18, 2019) 124 | 125 | * Add role validation to User creator, Fixes [#35](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/issues/35) 126 | 127 | ## v6.1.12 (May 17, 2019) 128 | 129 | * NPM updated 130 | 131 | ## v6.1.11 (May 14, 2019) 132 | 133 | * NPM updated 134 | 135 | ## v6.1.10 (May 5, 2019) 136 | 137 | * NPM updated 138 | 139 | ## v6.1.9 (Apr 24, 2019) 140 | 141 | * NPM updated 142 | 143 | ## v6.1.8 (Apr 14, 2019) 144 | 145 | * NPM updated 146 | 147 | ## v6.1.7 (Apr 11, 2019) 148 | 149 | * NPM updated 150 | 151 | ## v6.1.6 (Apr 9, 2019) 152 | 153 | * README.md updated 154 | 155 | ## v6.1.5 (Apr 9, 2019) 156 | 157 | * README.md updated 158 | 159 | ## v6.1.4 (Apr 8, 2019) 160 | 161 | * NPM updated 162 | 163 | ## v6.1.3 (Apr 7, 2019) 164 | 165 | * NPM updated 166 | 167 | ## v6.1.2 (Apr 2, 2019) 168 | 169 | * NPM updated 170 | 171 | ## v6.1.1 (Mar 30, 2019) 172 | 173 | * NPM updated 174 | 175 | ## v6.1.0 (Mar 29, 2019) 176 | 177 | * ENHANCEMENT: Refresh token endpoint now works as GET instead of POST 178 | 179 | ## v6.0.0 (Mar 28, 2019) 180 | 181 | * BREAKING CHANGE: Token payload and expiration have changed 182 | * BREAKING CHANGE: Constant in .env changed from `JWT_EXPIRATION` to `JWT_EXPIRATION_IN_MINUTES` 183 | * FEATURE: Refresh token 184 | 185 | ## v5.0.1 (Mar 25, 2019) 186 | 187 | * NPM update 188 | 189 | ## v5.0.0 (Mar 25, 2019) 190 | 191 | * Big refactor 192 | * FIX: send emails with mailgun 193 | 194 | ## v4.0.14 (Mar 25, 2019) 195 | 196 | * Removed unused code 197 | 198 | ## v4.0.13 (Mar 25, 2019) 199 | 200 | * FIX: remark 201 | 202 | ## v4.0.12 (Mar 25, 2019) 203 | 204 | * FIX: Convert an email in request to lowercase 205 | 206 | ## v4.0.11 (Mar 25, 2019) 207 | 208 | * README.md updated 209 | 210 | ## v4.0.10 (Mar 25, 2019) 211 | 212 | * README.md updated 213 | 214 | ## v4.0.9 (Mar 25, 2019) 215 | 216 | * README.md updated 217 | 218 | ## v4.0.8 (Mar 24, 2019) 219 | 220 | * Removed normalizeEmail() function from validator.js. It was removing dots from email addresses. New function in utils to convert an email in request to lowercase. Fixes [#11](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/issues/11) 221 | 222 | ## v4.0.7 (Mar 18, 2019) 223 | 224 | * Travis CI changes 225 | 226 | ## v4.0.6 (Mar 18, 2019) 227 | 228 | * CHANGELOG updated 229 | 230 | ## v4.0.5 (Mar 18, 2019) 231 | 232 | * Fix itemAlreadyExists refactor 233 | 234 | ## v4.0.4 (Mar 18, 2019) 235 | 236 | * itemAlreadyExists refactor 237 | 238 | ## v4.0.3 (Mar 18, 2019) 239 | 240 | * itemAlreadyExists refactor 241 | 242 | ## v4.0.2 (Mar 18, 2019) 243 | 244 | * itemNotFound refactor 245 | 246 | ## v4.0.1 (Mar 18, 2019) 247 | 248 | * Refactor emailer 249 | 250 | ## v4.0.0 (Mar 18, 2019) 251 | 252 | * Big refactor 253 | * NPM update 254 | 255 | ## v3.0.4 (Mar 17, 2019) 256 | 257 | * Bumped to v3.0.4 258 | 259 | ## v3.0.3 (Mar 17, 2019) 260 | 261 | * Use of remark to format markdown files 262 | 263 | ## v3.0.2 (Mar 17, 2019) 264 | 265 | * Use of remark to format markdown files 266 | * Fix: use of parseInt now provides a base 267 | 268 | ## v3.0.1 (Mar 15, 2019) 269 | 270 | * NPM updated 271 | * README.md updated 272 | 273 | ## v3.0.0 (Mar 15, 2019) 274 | 275 | * Demo added 276 | 277 | ## v2.3.3 (Mar 15, 2019) 278 | 279 | * Enable Redis based on env variable 280 | * API '/' route now renders an html view 281 | 282 | ## v2.3.2 (Mar 14, 2019) 283 | 284 | * Test for npm publish 285 | 286 | ## v2.3.1 (Mar 14, 2019) 287 | 288 | * Migrated to travis.com 289 | 290 | ## v2.3.0 (Mar 13, 2019) 291 | 292 | * Added verification in response in test and development env 293 | * Added verification for default admin user in seeding 294 | * Added tests for cities and users with filters 295 | * All functions documentated with JSDoc 296 | * base.js renamed to utils.js 297 | 298 | ## v2.2.8 (Mar 12, 2019) 299 | 300 | * Only builds in travis when tag is present 301 | 302 | ## v2.2.7 (Mar 12, 2019) 303 | 304 | * Verification code is showed on development and testing environments 305 | * NPM updated 306 | 307 | ## v2.2.6 (Mar 11, 2019) 308 | 309 | * Use of travis ci to automate deploy to npm 310 | * Added badge for tags in README.md 311 | 312 | ## v2.2.2 (Mar 11, 2019) 313 | 314 | * Use of travis ci to automate build and deploy 315 | * Added badge for travis build in README.md 316 | 317 | ## v2.2.1 (Mar 10, 2019) 318 | 319 | * Added badge for npm downloads in README.md 320 | 321 | ## v2.2.0 (Mar 10, 2019) 322 | 323 | * Filtering from multiple fields redesigned 324 | 325 | ## v2.1.10 (Mar 10, 2019) 326 | 327 | * NPM updated 328 | * FIX: creation of users were not saving data that validator was asking 329 | 330 | ## v2.1.9 (Mar 9, 2019) 331 | 332 | * NPM run lint added 333 | 334 | ## v2.1.8 (Mar 9, 2019) 335 | 336 | * New implementation for query on cities and users 337 | * More data on seeding 338 | * NPM updated 339 | 340 | ## v2.1.7 (Mar 4, 2019) 341 | 342 | * More tests added 343 | 344 | ## v2.1.6 (Mar 4, 2019) 345 | 346 | * Better testing 347 | 348 | ## v2.1.5 (Mar 4, 2019) 349 | 350 | * Istambul nyc code coverage added 351 | 352 | ## v2.1.4 (Mar 4, 2019) 353 | 354 | * Verification added only in tests responses at registration and forgot password 355 | * NPM updated 356 | * FIXED: User creation locale param was missing 357 | 358 | ## v2.1.3 (Mar 2, 2019) 359 | 360 | * Verification removed from responses at registration and forgot password (They were being used for testing and somehow made it here) 361 | 362 | ## v2.1.2 (Mar 2, 2019) 363 | 364 | * FEATURE: Install nodemon in devDependencies [#9](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/issues/9) 365 | * Typos 366 | * README.md updated 367 | 368 | ## v2.1.1 (Feb 27, 2019) 369 | 370 | * README.md updated 371 | 372 | ## v2.1.0 (Feb 27, 2019) 373 | 374 | * i18n added for registration email and forgot password email 375 | * Typos 376 | 377 | ## v2.0.1 (Feb 27, 2019) 378 | 379 | * Error response regression 380 | * NPM updated 381 | 382 | ## v2.0.0 (Feb 24, 2019) 383 | 384 | * Breaking changes for success and error responses 385 | * Added new endpoint in /profile/changePassword 386 | * Fixes in tests 387 | * Fixes in validations 388 | * NPM updated 389 | 390 | ## v1.2.12 (Feb 18, 2019) 391 | 392 | * NPM updated 393 | * CHANGELOG fixes 394 | * Typos 395 | 396 | ## v1.2.11 (Feb 11, 2019) 397 | 398 | * NPM updated 399 | * Removed pm2 from start script in package.json 400 | * server.js now inits redis stuff only in production 401 | 402 | ## v1.2.10 (Feb 9, 2019) 403 | 404 | * package.json updated 405 | 406 | ## v1.2.9 (Feb 9, 2019) 407 | 408 | * CHANGELOG updated 409 | 410 | ## v1.2.8 (Feb 9, 2019) 411 | 412 | * NPM updated 413 | 414 | ## v1.2.7 (Dec 4, 2018) 415 | 416 | * FIXED: Error message standarization [#6](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/issues/6) 417 | * Role property is returned in profile 418 | 419 | ## v1.2.6 (Dec 1, 2018) 420 | 421 | * FIXED: Password length validation in profile [#5](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/issues/5) 422 | * Role property is returned in profile 423 | 424 | ## v1.2.5 (Dec 1, 2018) 425 | 426 | * Comments 427 | 428 | ## v1.2.4 (Dec 1, 2018) 429 | 430 | * FIXED: Not standardized response on error [#4](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/issues/4) 431 | * NPM updated 432 | 433 | ## v1.2.3 (Nov 28, 2018) 434 | 435 | * FIXED: Password not encrypted when updating in profile [#3](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/issues/3) 436 | * NPM updated 437 | 438 | ## v1.2.2 (Nov 8, 2018) 439 | 440 | * NPM updated 441 | 442 | ## v1.2.1 (Nov 5, 2018) 443 | 444 | * Cache API responses only in production mode 445 | 446 | ## v1.2.0 (Nov 5, 2018) 447 | 448 | * Use of REDIS to cache API responses 449 | * NPM updated 450 | 451 | ## v1.1.3 (Oct 24, 2018) 452 | 453 | * Seeding Fix due to changes on new mongo-seeding package 454 | 455 | ## v1.1.2 (Oct 23, 2018) 456 | 457 | * NPM updated 458 | 459 | ## v1.1.1 (Sep 28, 2018) 460 | 461 | * Clean and Seed with async/await 462 | * Fixes 463 | 464 | ## v1.0.1 (Sep 21, 2018) 465 | 466 | * Added keywords to package.json 467 | 468 | ## v1.0.0 (Sep 20, 2018) 469 | 470 | * First stable release 471 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at \ The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | 47 | [version]: http://contributor-covenant.org/version/1/4/ 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Awesome! We're happy that you want to contribute. 4 | 5 | Make sure that you're read and understand the [Code of Conduct](CODE_OF_CONDUCT.md). 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daniel Avellaneda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js express.js MongoDB JWT REST API - Basic Project Skeleton 2 | 3 | [![Author](http://img.shields.io/badge/author-@davellanedam-blue.svg?style=flat-square)](https://twitter.com/davellanedam) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/blob/master/LICENSE) 5 | [![Tag](https://img.shields.io/github/tag/davellanedam/node-express-mongodb-jwt-rest-api-skeleton.svg?style=flat-square)](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/tags) 6 | [![Travis](https://img.shields.io/travis/com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton.svg?style=flat-square)]() 7 | [![npm downloads](https://img.shields.io/npm/dt/node-express-mongodb-jwt-rest-api-skeleton.svg?style=flat-square\&label=npm%20downloads)]() 8 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/fb6f20533c0f41b6b00da95ba634cd5e)](https://www.codacy.com/app/davellanedam/node-express-mongodb-jwt-rest-api-skeleton?utm_source=github.com\&utm_medium=referral\&utm_content=davellanedam/node-express-mongodb-jwt-rest-api-skeleton\&utm_campaign=Badge_Grade) 9 | 10 | ## Getting started 11 | 12 | This is a basic API REST skeleton written on JavaScript using async/await. Great for building a starter web API for your front-end (Android, iOS, Vue, react, angular, or anything that can consume an API) 13 | 14 | This project is created to help other developers create a **basic REST API in an easy way with Node.js**. This basic example shows how powerful and simple JavaScript can be. Do you want to contribute? Pull requests are always welcome to show more features. 15 | 16 | ## Buy me a coffee 17 | 18 | Hi! I'm Daniel Avellaneda, I'm an open source enthusiast and devote my free time to building projects in this field. 19 | 20 | I'm the creator and maintainer of [node-express-mongodb-jwt-rest-api-skeleton](https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/blob/master/README.md) and [vue-skeleton-mvp](https://github.com/davellanedam/vue-skeleton-mvp/blob/master/README.md) 21 | 22 | These projects are a "starter web app kit" for any developer who wants to build their own app without starting from scratch: API + Frontend 23 | 24 | Both projects have been downloaded thousands of times by web developers around the world. 25 | 26 | I'm doing my best to provide you a good experience when using my apps, so if you like what I'm doing and wish to say "thanks!", please buy me a coffee :coffee: 27 | 28 | Buy Me A Coffee 29 | 30 | Feel free to send me a tweet , share this with others or make a pull request 31 | 32 | ## Features 33 | 34 | * Multiple environment ready (development, production) 35 | * Custom email/password user system with basic security and blocking for preventing brute force attacks. 36 | * Compressed responses. 37 | * Secured HTTP headers. 38 | * CORS ready. 39 | * Cache ready (Redis). 40 | * HTTP request logger in development mode. 41 | * i18n ready (for sending emails in multiple languages). 42 | * User roles. 43 | * Pagination ready. 44 | * User profile. 45 | * Users list for admin area. 46 | * Cities model and controller example. 47 | * Login access log with IP, browser and country location (for country it looks for the header `cf-ipcountry` that CloudFlare creates when protecting your website). 48 | * API autogenerated documentation by Postman. 49 | * API collection example for Postman. 50 | * Testing with mocha/chai for API endpoints. 51 | * NPM scripts for cleaning and seeding the MongoDB database. 52 | * NPM script for keeping good source code formatting using prettier and ESLint. 53 | * Use of ESLint for good coding practices. 54 | * Mailer example with Nodemailer and Mailgun. 55 | * Ability to refresh token 56 | * JWT Tokens, make requests with a token after login with `Authorization` header with value `Bearer yourToken` where `yourToken` is the **signed and encrypted token** given in the response from the login process. 57 | 58 | ## Requirements 59 | 60 | * Node.js **10+** 61 | * MongoDB **3.6+** 62 | * Redis **5.0+** 63 | 64 | ## Demo 65 | 66 | A demo of this API is located at: 67 | 68 | ### Login credentials 69 | 70 | email: `admin@admin.com`\ 71 | password: `12345` 72 | 73 | **IMPORTANT:** Database resets every 30 mins like "12:00am, 12:30am, 1:00am" and so on. So anything you do with the API will be lost after a short time. 74 | 75 | [API documentation](###api-documentation)\ 76 | [Postman API example collection](###postman-api-example-collection)\ 77 | If you want to test it don´t forget to change the server variable to:\ 78 | `https://api-demo.daniel-avellaneda.com` 79 | 80 | Demo is also linked to a VueJS project that shows how this API can be integrated to a frontend that is able to consume an API.\ 81 | Repo is here: \ 82 | Running demo is here: 83 | 84 | ## How to install 85 | 86 | ### Using Git (recommended) 87 | 88 | 1. Clone the project from github. Change "myproject" to your project name. 89 | 90 | ```bash 91 | git clone https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton.git ./myproject 92 | ``` 93 | 94 | ### Using manual download ZIP 95 | 96 | 1. Download repository 97 | 2. Uncompress to your desired directory 98 | 99 | ### Install npm dependencies after installing (Git or manual download) 100 | 101 | ```bash 102 | cd myproject 103 | npm install 104 | npm update 105 | ``` 106 | 107 | ### Setting up environments (development or production) 108 | 109 | 1. In the root this repository you will find a file named `.env.example` 110 | 2. Create a new file by copying and pasting the file and then renaming it to just `.env` 111 | 3. The file `.env` is already ignored, so you never commit your credentials. 112 | 4. Change the values of the file to your environment (development or production) 113 | 5. Upload the `.env` to your environment server(development or production) 114 | 6. If you use the postman collection to try the endpoints, change value of the variable `server` on your environment to the url of your server, for development mode use 115 | 116 | **IMPORTANT:** By default token expires in 3 days (4320 minutes set in .env.example). You can refresh token at endpoint GET /token. If everything it´s ok you will get a new token. 117 | 118 | ### Mailer 119 | 120 | To ensure the deliverability of emails sent by this API, `Mailgun` is used for mailing users when they sign up, so if you want to use that feature go sign up at their website 121 | 122 | If you want to try a different method it´s ok, I used for this API and they have different transport methods like: smtp. 123 | 124 | ### i18n 125 | 126 | Language is automatically detected from `Accept-Language` header on the request. So either you send locale manually on the request or your browser will send its default, if `Accept-Language` header is not sent then it will use `en` locale as default. 127 | 128 | ## How to run 129 | 130 | ### Database cleaning and seeding samples 131 | 132 | There are 3 available commands for this: `fresh`, `clean` and `seed`. 133 | 134 | ```bash 135 | npm run command 136 | ``` 137 | 138 | * `fresh` cleans and then seeds the database with dynamic data. 139 | * `clean` cleans the database. 140 | * `seed` seeds the database with dynamic data. 141 | 142 | ### Running in development mode (lifting API server) 143 | 144 | ```bash 145 | npm run dev 146 | ``` 147 | 148 | You will know server is running by checking the output of the command `npm run dev` 149 | 150 | ```bash 151 | **************************** 152 | * Starting Server 153 | * Port: 3000 154 | * NODE_ENV: development 155 | * Database: MongoDB 156 | * DB Connection: OK 157 | **************************** 158 | ``` 159 | 160 | ### Running tests 161 | 162 | It´s a good practice to do tests at your code, so a sample of how to do that in `mocha/chai` is also included in the `/test` directory 163 | 164 | ```bash 165 | npm run test 166 | ``` 167 | 168 | ### Formatting code 169 | 170 | Format your code with prettier by typing: 171 | 172 | ```bash 173 | npm run format 174 | ``` 175 | 176 | ### Formatting markdown files 177 | 178 | Format all your markdown files with remark by typing: 179 | 180 | ```bash 181 | npm run remark 182 | ``` 183 | 184 | ### Linting code 185 | 186 | Lint your code with ESLint by typing: 187 | 188 | ```bash 189 | npm run lint 190 | ``` 191 | 192 | ## Usage 193 | 194 | Once everything is set up to test API routes either use Postman or any other api testing application. Default username/password combination for login is `admin@admin.com/12345`. 195 | 196 | ### API documentation 197 | 198 | 199 | 200 | ### Postman API example collection 201 | 202 | You can import the example collection to Postman. To import, click the import button located and select `postman-example.json` located within the root directory. 203 | 204 | Go to `manage environments` to create environments for development, production, etc. On each of the environments you create you will need to: 205 | 206 | 1. Create a new key `authToken` and within the `/login` request this value is automatically updated after a successfull login through a script located in the `tests` tab. Each time you make a request to the API it will send `Authorization` header with the `token` value in the request, you can check this on the headers of users or cities endpoints in the Postman example. 207 | 208 | 2. Create a second key `server` with the url of your server, for development mode use 209 | 210 | This is a REST API, so it works using the following HTTP methods: 211 | 212 | * GET (Read): Gets a list of items, or a single item 213 | * POST (Create): Creates an item 214 | * PATCH (Update): Updates an item 215 | * DELETE: Deletes an item 216 | 217 | ### Creating new models 218 | 219 | If you need to add more models to the project just create a new file in `/app/models/` and it will be loaded dynamically. 220 | 221 | ### Creating new routes 222 | 223 | If you need to add more routes to the project just create a new file in `/app/routes/` and it will be loaded dynamically. 224 | 225 | ### Creating new controllers 226 | 227 | When you create a new controller, try to also create another folder with validations and helpers. Ex. `/countries`, `/countries/validators` and `/countries/helpers`. An example of this is included in the repository. 228 | 229 | ## Bugs or improvements 230 | 231 | Feel free to report any bugs or improvements. Pull requests are always welcome. 232 | 233 | ## License 234 | 235 | This project is open-sourced software licensed under the MIT License. See the LICENSE file for more information. 236 | -------------------------------------------------------------------------------- /app/controllers/auth/forgotPassword.js: -------------------------------------------------------------------------------- 1 | const { matchedData } = require('express-validator') 2 | const { 3 | findUser, 4 | forgotPasswordResponse, 5 | saveForgotPassword 6 | } = require('./helpers') 7 | const { handleError } = require('../../middleware/utils') 8 | const { sendResetPasswordEmailMessage } = require('../../middleware/emailer') 9 | 10 | /** 11 | * Forgot password function called by route 12 | * @param {Object} req - request object 13 | * @param {Object} res - response object 14 | */ 15 | const forgotPassword = async (req, res) => { 16 | try { 17 | // Gets locale from header 'Accept-Language' 18 | const locale = req.getLocale() 19 | const data = matchedData(req) 20 | await findUser(data.email) 21 | const item = await saveForgotPassword(req) 22 | sendResetPasswordEmailMessage(locale, item) 23 | res.status(200).json(forgotPasswordResponse(item)) 24 | } catch (error) { 25 | handleError(res, error) 26 | } 27 | } 28 | 29 | module.exports = { forgotPassword } 30 | -------------------------------------------------------------------------------- /app/controllers/auth/getRefreshToken.js: -------------------------------------------------------------------------------- 1 | const { 2 | getUserIdFromToken, 3 | findUserById, 4 | saveUserAccessAndReturnToken 5 | } = require('./helpers') 6 | const { isIDGood, handleError } = require('../../middleware/utils') 7 | 8 | /** 9 | * Refresh token function called by route 10 | * @param {Object} req - request object 11 | * @param {Object} res - response object 12 | */ 13 | const getRefreshToken = async (req, res) => { 14 | try { 15 | const tokenEncrypted = req.headers.authorization 16 | .replace('Bearer ', '') 17 | .trim() 18 | let userId = await getUserIdFromToken(tokenEncrypted) 19 | userId = await isIDGood(userId) 20 | const user = await findUserById(userId) 21 | const token = await saveUserAccessAndReturnToken(req, user) 22 | // Removes user info from response 23 | delete token.user 24 | res.status(200).json(token) 25 | } catch (error) { 26 | handleError(res, error) 27 | } 28 | } 29 | 30 | module.exports = { getRefreshToken } 31 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/blockIsExpired.js: -------------------------------------------------------------------------------- 1 | const LOGIN_ATTEMPTS = 5 2 | 3 | /** 4 | * Checks that login attempts are greater than specified in constant and also that blockexpires is less than now 5 | * @param {Object} user - user object 6 | */ 7 | const blockIsExpired = ({ loginAttempts = 0, blockExpires = '' }) => 8 | loginAttempts > LOGIN_ATTEMPTS && blockExpires <= new Date() 9 | 10 | module.exports = { blockIsExpired } 11 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/blockUser.js: -------------------------------------------------------------------------------- 1 | const { addHours } = require('date-fns') 2 | const HOURS_TO_BLOCK = 2 3 | 4 | const { buildErrObject } = require('../../../middleware/utils') 5 | 6 | /** 7 | * Blocks a user by setting blockExpires to the specified date based on constant HOURS_TO_BLOCK 8 | * @param {Object} user - user object 9 | */ 10 | const blockUser = (user = {}) => { 11 | return new Promise((resolve, reject) => { 12 | user.blockExpires = addHours(new Date(), HOURS_TO_BLOCK) 13 | user.save((err, result) => { 14 | if (err) { 15 | return reject(buildErrObject(422, err.message)) 16 | } 17 | if (result) { 18 | return resolve(buildErrObject(409, 'BLOCKED_USER')) 19 | } 20 | }) 21 | }) 22 | } 23 | 24 | module.exports = { blockUser } 25 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/checkLoginAttemptsAndBlockExpires.js: -------------------------------------------------------------------------------- 1 | const { blockIsExpired } = require('./blockIsExpired') 2 | const { buildErrObject } = require('../../../middleware/utils') 3 | 4 | /** 5 | * 6 | * @param {Object} user - user object. 7 | */ 8 | const checkLoginAttemptsAndBlockExpires = (user = {}) => { 9 | return new Promise((resolve, reject) => { 10 | // Let user try to login again after blockexpires, resets user loginAttempts 11 | if (blockIsExpired(user)) { 12 | user.loginAttempts = 0 13 | user.save((err, result) => { 14 | if (err) { 15 | return reject(buildErrObject(422, err.message)) 16 | } 17 | if (result) { 18 | return resolve(true) 19 | } 20 | }) 21 | } 22 | // User is not blocked, check password (normal behaviour) 23 | resolve(true) 24 | }) 25 | } 26 | 27 | module.exports = { checkLoginAttemptsAndBlockExpires } 28 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/checkPermissions.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user') 2 | const { itemNotFound, buildErrObject } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Checks against user if has quested role 6 | * @param {Object} data - data object 7 | * @param {*} next - next callback 8 | */ 9 | const checkPermissions = ({ id = '', roles = [] }, next) => { 10 | return new Promise((resolve, reject) => { 11 | User.findById(id, async (err, result) => { 12 | try { 13 | await itemNotFound(err, result, 'USER_NOT_FOUND') 14 | if (roles.indexOf(result.role) > -1) { 15 | return resolve(next()) 16 | } 17 | reject(buildErrObject(401, 'UNAUTHORIZED')) 18 | } catch (error) { 19 | reject(error) 20 | } 21 | }) 22 | }) 23 | } 24 | 25 | module.exports = { checkPermissions } 26 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/findForgotPassword.js: -------------------------------------------------------------------------------- 1 | const ForgotPassword = require('../../../models/forgotPassword') 2 | const { itemNotFound } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Checks if a forgot password verification exists 6 | * @param {string} id - verification id 7 | */ 8 | const findForgotPassword = (id = '') => { 9 | return new Promise((resolve, reject) => { 10 | ForgotPassword.findOne( 11 | { 12 | verification: id, 13 | used: false 14 | }, 15 | async (err, item) => { 16 | try { 17 | await itemNotFound(err, item, 'NOT_FOUND_OR_ALREADY_USED') 18 | resolve(item) 19 | } catch (error) { 20 | reject(error) 21 | } 22 | } 23 | ) 24 | }) 25 | } 26 | 27 | module.exports = { findForgotPassword } 28 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/findUser.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user') 2 | const { itemNotFound } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Finds user by email 6 | * @param {string} email - user´s email 7 | */ 8 | const findUser = (email = '') => { 9 | return new Promise((resolve, reject) => { 10 | User.findOne( 11 | { 12 | email 13 | }, 14 | 'password loginAttempts blockExpires name email role verified verification', 15 | async (err, item) => { 16 | try { 17 | await itemNotFound(err, item, 'USER_DOES_NOT_EXIST') 18 | resolve(item) 19 | } catch (error) { 20 | reject(error) 21 | } 22 | } 23 | ) 24 | }) 25 | } 26 | 27 | module.exports = { findUser } 28 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/findUserById.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user') 2 | const { itemNotFound } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Finds user by ID 6 | * @param {string} id - user´s id 7 | */ 8 | const findUserById = (userId = '') => { 9 | return new Promise((resolve, reject) => { 10 | User.findById(userId, async (err, item) => { 11 | try { 12 | await itemNotFound(err, item, 'USER_DOES_NOT_EXIST') 13 | resolve(item) 14 | } catch (error) { 15 | reject(error) 16 | } 17 | }) 18 | }) 19 | } 20 | 21 | module.exports = { findUserById } 22 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/findUserToResetPassword.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user') 2 | const { itemNotFound } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Finds user by email to reset password 6 | * @param {string} email - user email 7 | */ 8 | const findUserToResetPassword = (email = '') => { 9 | return new Promise((resolve, reject) => { 10 | User.findOne( 11 | { 12 | email 13 | }, 14 | async (err, user) => { 15 | try { 16 | await itemNotFound(err, user, 'NOT_FOUND') 17 | resolve(user) 18 | } catch (error) { 19 | reject(error) 20 | } 21 | } 22 | ) 23 | }) 24 | } 25 | 26 | module.exports = { findUserToResetPassword } 27 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/forgotPasswordResponse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds an object with created forgot password object, if env is development or testing exposes the verification 3 | * @param {Object} item - created forgot password object 4 | */ 5 | const forgotPasswordResponse = ({ email = '', verification = '' }) => { 6 | let data = { 7 | msg: 'RESET_EMAIL_SENT', 8 | email 9 | } 10 | if (process.env.NODE_ENV !== 'production') { 11 | data = { 12 | ...data, 13 | verification 14 | } 15 | } 16 | return data 17 | } 18 | 19 | module.exports = { forgotPasswordResponse } 20 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/generateToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const { encrypt } = require('../../../middleware/auth') 3 | 4 | /** 5 | * Generates a token 6 | * @param {Object} user - user object 7 | */ 8 | const generateToken = (user = '') => { 9 | try { 10 | // Gets expiration time 11 | const expiration = 12 | Math.floor(Date.now() / 1000) + 60 * process.env.JWT_EXPIRATION_IN_MINUTES 13 | 14 | // returns signed and encrypted token 15 | return encrypt( 16 | jwt.sign( 17 | { 18 | data: { 19 | _id: user 20 | }, 21 | exp: expiration 22 | }, 23 | process.env.JWT_SECRET 24 | ) 25 | ) 26 | } catch (error) { 27 | throw error 28 | } 29 | } 30 | 31 | module.exports = { generateToken } 32 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/getUserIdFromToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const { buildErrObject } = require('../../../middleware/utils') 3 | const { decrypt } = require('../../../middleware/auth') 4 | 5 | /** 6 | * Gets user id from token 7 | * @param {string} token - Encrypted and encoded token 8 | */ 9 | const getUserIdFromToken = (token = '') => { 10 | return new Promise((resolve, reject) => { 11 | // Decrypts, verifies and decode token 12 | jwt.verify(decrypt(token), process.env.JWT_SECRET, (err, decoded) => { 13 | if (err) { 14 | reject(buildErrObject(409, 'BAD_TOKEN')) 15 | } 16 | resolve(decoded.data._id) 17 | }) 18 | }) 19 | } 20 | 21 | module.exports = { getUserIdFromToken } 22 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/index.js: -------------------------------------------------------------------------------- 1 | const { blockIsExpired } = require('./blockIsExpired') 2 | const { blockUser } = require('./blockUser') 3 | const { 4 | checkLoginAttemptsAndBlockExpires 5 | } = require('./checkLoginAttemptsAndBlockExpires') 6 | const { checkPermissions } = require('./checkPermissions') 7 | const { findForgotPassword } = require('./findForgotPassword') 8 | const { findUser } = require('./findUser') 9 | const { findUserById } = require('./findUserById') 10 | const { findUserToResetPassword } = require('./findUserToResetPassword') 11 | const { forgotPasswordResponse } = require('./forgotPasswordResponse') 12 | const { generateToken } = require('./generateToken') 13 | const { getUserIdFromToken } = require('./getUserIdFromToken') 14 | const { markResetPasswordAsUsed } = require('./markResetPasswordAsUsed') 15 | const { passwordsDoNotMatch } = require('./passwordsDoNotMatch') 16 | const { registerUser } = require('./registerUser') 17 | const { returnRegisterToken } = require('./returnRegisterToken') 18 | const { saveForgotPassword } = require('./saveForgotPassword') 19 | const { saveLoginAttemptsToDB } = require('./saveLoginAttemptsToDB') 20 | const { 21 | saveUserAccessAndReturnToken 22 | } = require('./saveUserAccessAndReturnToken') 23 | const { setUserInfo } = require('./setUserInfo') 24 | const { updatePassword } = require('./updatePassword') 25 | const { userIsBlocked } = require('./userIsBlocked') 26 | const { verificationExists } = require('./verificationExists') 27 | const { verifyUser } = require('./verifyUser') 28 | 29 | module.exports = { 30 | blockIsExpired, 31 | blockUser, 32 | checkLoginAttemptsAndBlockExpires, 33 | checkPermissions, 34 | findForgotPassword, 35 | findUser, 36 | findUserById, 37 | findUserToResetPassword, 38 | forgotPasswordResponse, 39 | generateToken, 40 | getUserIdFromToken, 41 | markResetPasswordAsUsed, 42 | passwordsDoNotMatch, 43 | registerUser, 44 | returnRegisterToken, 45 | saveForgotPassword, 46 | saveLoginAttemptsToDB, 47 | saveUserAccessAndReturnToken, 48 | setUserInfo, 49 | updatePassword, 50 | userIsBlocked, 51 | verificationExists, 52 | verifyUser 53 | } 54 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/markResetPasswordAsUsed.js: -------------------------------------------------------------------------------- 1 | const { 2 | getIP, 3 | getBrowserInfo, 4 | getCountry, 5 | itemNotFound, 6 | buildSuccObject 7 | } = require('../../../middleware/utils') 8 | 9 | /** 10 | * Marks a request to reset password as used 11 | * @param {Object} req - request object 12 | * @param {Object} forgot - forgot object 13 | */ 14 | const markResetPasswordAsUsed = (req = {}, forgot = {}) => { 15 | return new Promise((resolve, reject) => { 16 | forgot.used = true 17 | forgot.ipChanged = getIP(req) 18 | forgot.browserChanged = getBrowserInfo(req) 19 | forgot.countryChanged = getCountry(req) 20 | forgot.save(async (err, item) => { 21 | try { 22 | await itemNotFound(err, item, 'NOT_FOUND') 23 | resolve(buildSuccObject('PASSWORD_CHANGED')) 24 | } catch (error) { 25 | reject(error) 26 | } 27 | }) 28 | }) 29 | } 30 | 31 | module.exports = { markResetPasswordAsUsed } 32 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/passwordsDoNotMatch.js: -------------------------------------------------------------------------------- 1 | const { saveLoginAttemptsToDB } = require('./saveLoginAttemptsToDB') 2 | const { blockUser } = require('./blockUser') 3 | const { buildErrObject } = require('../../../middleware/utils') 4 | const LOGIN_ATTEMPTS = 5 5 | 6 | /** 7 | * Adds one attempt to loginAttempts, then compares loginAttempts with the constant LOGIN_ATTEMPTS, if is less returns wrong password, else returns blockUser function 8 | * @param {Object} user - user object 9 | */ 10 | const passwordsDoNotMatch = async (user = {}) => { 11 | return new Promise(async (resolve, reject) => { 12 | try { 13 | user.loginAttempts += 1 14 | await saveLoginAttemptsToDB(user) 15 | if (user.loginAttempts <= LOGIN_ATTEMPTS) { 16 | return reject(buildErrObject(409, 'WRONG_PASSWORD')) 17 | } 18 | 19 | resolve(await blockUser(user)) 20 | } catch (error) { 21 | throw error 22 | } 23 | }) 24 | } 25 | 26 | module.exports = { passwordsDoNotMatch } 27 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/registerUser.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid') 2 | const User = require('../../../models/user') 3 | const { buildErrObject } = require('../../../middleware/utils') 4 | 5 | /** 6 | * Registers a new user in database 7 | * @param {Object} req - request object 8 | */ 9 | const registerUser = (req = {}) => { 10 | return new Promise((resolve, reject) => { 11 | const user = new User({ 12 | name: req.name, 13 | email: req.email, 14 | password: req.password, 15 | verification: uuid.v4() 16 | }) 17 | user.save((err, item) => { 18 | if (err) { 19 | reject(buildErrObject(422, err.message)) 20 | } 21 | resolve(item) 22 | }) 23 | }) 24 | } 25 | 26 | module.exports = { registerUser } 27 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/returnRegisterToken.js: -------------------------------------------------------------------------------- 1 | const { generateToken } = require('./generateToken') 2 | 3 | /** 4 | * Builds the registration token 5 | * @param {Object} item - user object that contains created id 6 | * @param {Object} userInfo - user object 7 | */ 8 | const returnRegisterToken = ( 9 | { _id = '', verification = '' }, 10 | userInfo = {} 11 | ) => { 12 | return new Promise((resolve) => { 13 | if (process.env.NODE_ENV !== 'production') { 14 | userInfo.verification = verification 15 | } 16 | const data = { 17 | token: generateToken(_id), 18 | user: userInfo 19 | } 20 | resolve(data) 21 | }) 22 | } 23 | 24 | module.exports = { returnRegisterToken } 25 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/saveForgotPassword.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid') 2 | const ForgotPassword = require('../../../models/forgotPassword') 3 | const { 4 | getIP, 5 | getBrowserInfo, 6 | getCountry, 7 | buildErrObject 8 | } = require('../../../middleware/utils') 9 | 10 | /** 11 | * Creates a new password forgot 12 | * @param {Object} req - request object 13 | */ 14 | const saveForgotPassword = (req = {}) => { 15 | return new Promise((resolve, reject) => { 16 | const forgot = new ForgotPassword({ 17 | email: req.body.email, 18 | verification: uuid.v4(), 19 | ipRequest: getIP(req), 20 | browserRequest: getBrowserInfo(req), 21 | countryRequest: getCountry(req) 22 | }) 23 | forgot.save((err, item) => { 24 | if (err) { 25 | return reject(buildErrObject(422, err.message)) 26 | } 27 | resolve(item) 28 | }) 29 | }) 30 | } 31 | 32 | module.exports = { saveForgotPassword } 33 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/saveLoginAttemptsToDB.js: -------------------------------------------------------------------------------- 1 | const { buildErrObject } = require('../../../middleware/utils') 2 | 3 | /** 4 | * Saves login attempts to dabatabse 5 | * @param {Object} user - user object 6 | */ 7 | const saveLoginAttemptsToDB = (user = {}) => { 8 | return new Promise((resolve, reject) => { 9 | user.save((err, result) => { 10 | if (err) { 11 | return reject(buildErrObject(422, err.message)) 12 | } 13 | if (result) { 14 | resolve(true) 15 | } 16 | }) 17 | }) 18 | } 19 | 20 | module.exports = { saveLoginAttemptsToDB } 21 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/saveUserAccessAndReturnToken.js: -------------------------------------------------------------------------------- 1 | const UserAccess = require('../../../models/userAccess') 2 | const { setUserInfo } = require('./setUserInfo') 3 | const { generateToken } = require('./generateToken') 4 | const { 5 | getIP, 6 | getBrowserInfo, 7 | getCountry, 8 | buildErrObject 9 | } = require('../../../middleware/utils') 10 | 11 | /** 12 | * Saves a new user access and then returns token 13 | * @param {Object} req - request object 14 | * @param {Object} user - user object 15 | */ 16 | const saveUserAccessAndReturnToken = (req = {}, user = {}) => { 17 | return new Promise((resolve, reject) => { 18 | const userAccess = new UserAccess({ 19 | email: user.email, 20 | ip: getIP(req), 21 | browser: getBrowserInfo(req), 22 | country: getCountry(req) 23 | }) 24 | userAccess.save(async (err) => { 25 | try { 26 | if (err) { 27 | return reject(buildErrObject(422, err.message)) 28 | } 29 | const userInfo = await setUserInfo(user) 30 | // Returns data with access token 31 | resolve({ 32 | token: generateToken(user._id), 33 | user: userInfo 34 | }) 35 | } catch (error) { 36 | reject(error) 37 | } 38 | }) 39 | }) 40 | } 41 | 42 | module.exports = { saveUserAccessAndReturnToken } 43 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/setUserInfo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an object with user info 3 | * @param {Object} req - request object 4 | */ 5 | const setUserInfo = (req = {}) => { 6 | return new Promise((resolve) => { 7 | let user = { 8 | _id: req._id, 9 | name: req.name, 10 | email: req.email, 11 | role: req.role, 12 | verified: req.verified 13 | } 14 | // Adds verification for testing purposes 15 | if (process.env.NODE_ENV !== 'production') { 16 | user = { 17 | ...user, 18 | verification: req.verification 19 | } 20 | } 21 | resolve(user) 22 | }) 23 | } 24 | 25 | module.exports = { setUserInfo } 26 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/updatePassword.js: -------------------------------------------------------------------------------- 1 | const { itemNotFound } = require('../../../middleware/utils') 2 | 3 | /** 4 | * Updates a user password in database 5 | * @param {string} password - new password 6 | * @param {Object} user - user object 7 | */ 8 | const updatePassword = (password = '', user = {}) => { 9 | return new Promise((resolve, reject) => { 10 | user.password = password 11 | user.save(async (err, item) => { 12 | try { 13 | await itemNotFound(err, item, 'NOT_FOUND') 14 | resolve(item) 15 | } catch (error) { 16 | reject(error) 17 | } 18 | }) 19 | }) 20 | } 21 | 22 | module.exports = { updatePassword } 23 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/userIsBlocked.js: -------------------------------------------------------------------------------- 1 | const { buildErrObject } = require('../../../middleware/utils') 2 | 3 | /** 4 | * Checks if blockExpires from user is greater than now 5 | * @param {Object} user - user object 6 | */ 7 | const userIsBlocked = (user = {}) => { 8 | return new Promise((resolve, reject) => { 9 | if (user.blockExpires > new Date()) { 10 | return reject(buildErrObject(409, 'BLOCKED_USER')) 11 | } 12 | resolve(true) 13 | }) 14 | } 15 | 16 | module.exports = { userIsBlocked } 17 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/verificationExists.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user') 2 | const { itemNotFound } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Checks if verification id exists for user 6 | * @param {string} id - verification id 7 | */ 8 | const verificationExists = (id = '') => { 9 | return new Promise((resolve, reject) => { 10 | User.findOne( 11 | { 12 | verification: id, 13 | verified: false 14 | }, 15 | async (err, user) => { 16 | try { 17 | await itemNotFound(err, user, 'NOT_FOUND_OR_ALREADY_VERIFIED') 18 | resolve(user) 19 | } catch (error) { 20 | reject(error) 21 | } 22 | } 23 | ) 24 | }) 25 | } 26 | 27 | module.exports = { verificationExists } 28 | -------------------------------------------------------------------------------- /app/controllers/auth/helpers/verifyUser.js: -------------------------------------------------------------------------------- 1 | const { buildErrObject } = require('../../../middleware/utils') 2 | 3 | /** 4 | * Verifies an user 5 | * @param {Object} user - user object 6 | */ 7 | const verifyUser = (user = {}) => { 8 | return new Promise((resolve, reject) => { 9 | user.verified = true 10 | user.save((err, item) => { 11 | if (err) { 12 | return reject(buildErrObject(422, err.message)) 13 | } 14 | resolve({ 15 | email: item.email, 16 | verified: item.verified 17 | }) 18 | }) 19 | }) 20 | } 21 | 22 | module.exports = { verifyUser } 23 | -------------------------------------------------------------------------------- /app/controllers/auth/index.js: -------------------------------------------------------------------------------- 1 | const { forgotPassword } = require('./forgotPassword') 2 | const { getRefreshToken } = require('./getRefreshToken') 3 | const { login } = require('./login') 4 | const { register } = require('./register') 5 | const { resetPassword } = require('./resetPassword') 6 | const { roleAuthorization } = require('./roleAuthorization') 7 | const { verify } = require('./verify') 8 | 9 | module.exports = { 10 | forgotPassword, 11 | getRefreshToken, 12 | login, 13 | register, 14 | resetPassword, 15 | roleAuthorization, 16 | verify 17 | } 18 | -------------------------------------------------------------------------------- /app/controllers/auth/login.js: -------------------------------------------------------------------------------- 1 | const { matchedData } = require('express-validator') 2 | 3 | const { 4 | findUser, 5 | userIsBlocked, 6 | checkLoginAttemptsAndBlockExpires, 7 | passwordsDoNotMatch, 8 | saveLoginAttemptsToDB, 9 | saveUserAccessAndReturnToken 10 | } = require('./helpers') 11 | 12 | const { handleError } = require('../../middleware/utils') 13 | const { checkPassword } = require('../../middleware/auth') 14 | 15 | /** 16 | * Login function called by route 17 | * @param {Object} req - request object 18 | * @param {Object} res - response object 19 | */ 20 | const login = async (req, res) => { 21 | try { 22 | const data = matchedData(req) 23 | const user = await findUser(data.email) 24 | await userIsBlocked(user) 25 | await checkLoginAttemptsAndBlockExpires(user) 26 | const isPasswordMatch = await checkPassword(data.password, user) 27 | if (!isPasswordMatch) { 28 | handleError(res, await passwordsDoNotMatch(user)) 29 | } else { 30 | // all ok, register access and return token 31 | user.loginAttempts = 0 32 | await saveLoginAttemptsToDB(user) 33 | res.status(200).json(await saveUserAccessAndReturnToken(req, user)) 34 | } 35 | } catch (error) { 36 | handleError(res, error) 37 | } 38 | } 39 | 40 | module.exports = { login } 41 | -------------------------------------------------------------------------------- /app/controllers/auth/register.js: -------------------------------------------------------------------------------- 1 | const { matchedData } = require('express-validator') 2 | 3 | const { registerUser, setUserInfo, returnRegisterToken } = require('./helpers') 4 | 5 | const { handleError } = require('../../middleware/utils') 6 | const { 7 | emailExists, 8 | sendRegistrationEmailMessage 9 | } = require('../../middleware/emailer') 10 | 11 | /** 12 | * Register function called by route 13 | * @param {Object} req - request object 14 | * @param {Object} res - response object 15 | */ 16 | const register = async (req, res) => { 17 | try { 18 | // Gets locale from header 'Accept-Language' 19 | const locale = req.getLocale() 20 | req = matchedData(req) 21 | const doesEmailExists = await emailExists(req.email) 22 | if (!doesEmailExists) { 23 | const item = await registerUser(req) 24 | const userInfo = await setUserInfo(item) 25 | const response = await returnRegisterToken(item, userInfo) 26 | sendRegistrationEmailMessage(locale, item) 27 | res.status(201).json(response) 28 | } 29 | } catch (error) { 30 | handleError(res, error) 31 | } 32 | } 33 | 34 | module.exports = { register } 35 | -------------------------------------------------------------------------------- /app/controllers/auth/resetPassword.js: -------------------------------------------------------------------------------- 1 | const { matchedData } = require('express-validator') 2 | const { 3 | findForgotPassword, 4 | findUserToResetPassword, 5 | updatePassword, 6 | markResetPasswordAsUsed 7 | } = require('./helpers') 8 | const { handleError } = require('../../middleware/utils') 9 | 10 | /** 11 | * Reset password function called by route 12 | * @param {Object} req - request object 13 | * @param {Object} res - response object 14 | */ 15 | const resetPassword = async (req, res) => { 16 | try { 17 | const data = matchedData(req) 18 | const forgotPassword = await findForgotPassword(data.id) 19 | const user = await findUserToResetPassword(forgotPassword.email) 20 | await updatePassword(data.password, user) 21 | const result = await markResetPasswordAsUsed(req, forgotPassword) 22 | res.status(200).json(result) 23 | } catch (error) { 24 | handleError(res, error) 25 | } 26 | } 27 | 28 | module.exports = { resetPassword } 29 | -------------------------------------------------------------------------------- /app/controllers/auth/roleAuthorization.js: -------------------------------------------------------------------------------- 1 | const { checkPermissions } = require('./helpers') 2 | 3 | const { handleError } = require('../../middleware/utils') 4 | 5 | /** 6 | * Roles authorization function called by route 7 | * @param {Array} roles - roles specified on the route 8 | */ 9 | const roleAuthorization = (roles) => async (req, res, next) => { 10 | try { 11 | const data = { 12 | id: req.user._id, 13 | roles 14 | } 15 | await checkPermissions(data, next) 16 | } catch (error) { 17 | handleError(res, error) 18 | } 19 | } 20 | 21 | module.exports = { roleAuthorization } 22 | -------------------------------------------------------------------------------- /app/controllers/auth/validators/index.js: -------------------------------------------------------------------------------- 1 | const { validateForgotPassword } = require('./validateForgotPassword') 2 | const { validateLogin } = require('./validateLogin') 3 | const { validateRegister } = require('./validateRegister') 4 | const { validateResetPassword } = require('./validateResetPassword') 5 | const { validateVerify } = require('./validateVerify') 6 | 7 | module.exports = { 8 | validateForgotPassword, 9 | validateLogin, 10 | validateRegister, 11 | validateResetPassword, 12 | validateVerify 13 | } 14 | -------------------------------------------------------------------------------- /app/controllers/auth/validators/validateForgotPassword.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates forgot password request 6 | */ 7 | const validateForgotPassword = [ 8 | check('email') 9 | .exists() 10 | .withMessage('MISSING') 11 | .not() 12 | .isEmpty() 13 | .withMessage('IS_EMPTY') 14 | .isEmail() 15 | .withMessage('EMAIL_IS_NOT_VALID'), 16 | (req, res, next) => { 17 | validateResult(req, res, next) 18 | } 19 | ] 20 | 21 | module.exports = { validateForgotPassword } 22 | -------------------------------------------------------------------------------- /app/controllers/auth/validators/validateLogin.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates register request 6 | */ 7 | /** 8 | * Validates login request 9 | */ 10 | const validateLogin = [ 11 | check('email') 12 | .exists() 13 | .withMessage('MISSING') 14 | .not() 15 | .isEmpty() 16 | .withMessage('IS_EMPTY') 17 | .isEmail() 18 | .withMessage('EMAIL_IS_NOT_VALID'), 19 | check('password') 20 | .exists() 21 | .withMessage('MISSING') 22 | .not() 23 | .isEmpty() 24 | .withMessage('IS_EMPTY') 25 | .isLength({ 26 | min: 5 27 | }) 28 | .withMessage('PASSWORD_TOO_SHORT_MIN_5'), 29 | (req, res, next) => { 30 | validateResult(req, res, next) 31 | } 32 | ] 33 | 34 | module.exports = { validateLogin } 35 | -------------------------------------------------------------------------------- /app/controllers/auth/validators/validateRegister.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates register request 6 | */ 7 | const validateRegister = [ 8 | check('name') 9 | .exists() 10 | .withMessage('MISSING') 11 | .not() 12 | .isEmpty() 13 | .withMessage('IS_EMPTY'), 14 | check('email') 15 | .exists() 16 | .withMessage('MISSING') 17 | .not() 18 | .isEmpty() 19 | .withMessage('IS_EMPTY') 20 | .isEmail() 21 | .withMessage('EMAIL_IS_NOT_VALID'), 22 | check('password') 23 | .exists() 24 | .withMessage('MISSING') 25 | .not() 26 | .isEmpty() 27 | .withMessage('IS_EMPTY') 28 | .isLength({ 29 | min: 5 30 | }) 31 | .withMessage('PASSWORD_TOO_SHORT_MIN_5'), 32 | (req, res, next) => { 33 | validateResult(req, res, next) 34 | } 35 | ] 36 | 37 | module.exports = { validateRegister } 38 | -------------------------------------------------------------------------------- /app/controllers/auth/validators/validateResetPassword.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates reset password request 6 | */ 7 | const validateResetPassword = [ 8 | check('id') 9 | .exists() 10 | .withMessage('MISSING') 11 | .not() 12 | .isEmpty() 13 | .withMessage('IS_EMPTY'), 14 | check('password') 15 | .exists() 16 | .withMessage('MISSING') 17 | .not() 18 | .isEmpty() 19 | .withMessage('IS_EMPTY') 20 | .isLength({ 21 | min: 5 22 | }) 23 | .withMessage('PASSWORD_TOO_SHORT_MIN_5'), 24 | (req, res, next) => { 25 | validateResult(req, res, next) 26 | } 27 | ] 28 | 29 | module.exports = { validateResetPassword } 30 | -------------------------------------------------------------------------------- /app/controllers/auth/validators/validateVerify.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates verify request 6 | */ 7 | const validateVerify = [ 8 | check('id') 9 | .exists() 10 | .withMessage('MISSING') 11 | .not() 12 | .isEmpty() 13 | .withMessage('IS_EMPTY'), 14 | (req, res, next) => { 15 | validateResult(req, res, next) 16 | } 17 | ] 18 | 19 | module.exports = { validateVerify } 20 | -------------------------------------------------------------------------------- /app/controllers/auth/verify.js: -------------------------------------------------------------------------------- 1 | const { matchedData } = require('express-validator') 2 | const { verificationExists, verifyUser } = require('./helpers') 3 | 4 | const { handleError } = require('../../middleware/utils') 5 | 6 | /** 7 | * Verify function called by route 8 | * @param {Object} req - request object 9 | * @param {Object} res - response object 10 | */ 11 | const verify = async (req, res) => { 12 | try { 13 | req = matchedData(req) 14 | const user = await verificationExists(req.id) 15 | res.status(200).json(await verifyUser(user)) 16 | } catch (error) { 17 | handleError(res, error) 18 | } 19 | } 20 | 21 | module.exports = { verify } 22 | -------------------------------------------------------------------------------- /app/controllers/cities/createCity.js: -------------------------------------------------------------------------------- 1 | const City = require('../../models/city') 2 | const { createItem } = require('../../middleware/db') 3 | const { handleError } = require('../../middleware/utils') 4 | const { matchedData } = require('express-validator') 5 | const { cityExists } = require('./helpers') 6 | 7 | /** 8 | * Create item function called by route 9 | * @param {Object} req - request object 10 | * @param {Object} res - response object 11 | */ 12 | const createCity = async (req, res) => { 13 | try { 14 | req = matchedData(req) 15 | const doesCityExists = await cityExists(req.name) 16 | if (!doesCityExists) { 17 | res.status(201).json(await createItem(req, City)) 18 | } 19 | } catch (error) { 20 | handleError(res, error) 21 | } 22 | } 23 | 24 | module.exports = { createCity } 25 | -------------------------------------------------------------------------------- /app/controllers/cities/deleteCity.js: -------------------------------------------------------------------------------- 1 | const City = require('../../models/city') 2 | const { matchedData } = require('express-validator') 3 | const { isIDGood, handleError } = require('../../middleware/utils') 4 | const { deleteItem } = require('../../middleware/db') 5 | 6 | /** 7 | * Delete item function called by route 8 | * @param {Object} req - request object 9 | * @param {Object} res - response object 10 | */ 11 | const deleteCity = async (req, res) => { 12 | try { 13 | req = matchedData(req) 14 | const id = await isIDGood(req.id) 15 | res.status(200).json(await deleteItem(id, City)) 16 | } catch (error) { 17 | handleError(res, error) 18 | } 19 | } 20 | 21 | module.exports = { deleteCity } 22 | -------------------------------------------------------------------------------- /app/controllers/cities/getAllCities.js: -------------------------------------------------------------------------------- 1 | const { handleError } = require('../../middleware/utils') 2 | const { getAllItemsFromDB } = require('./helpers') 3 | 4 | /** 5 | * Get all items function called by route 6 | * @param {Object} req - request object 7 | * @param {Object} res - response object 8 | */ 9 | const getAllCities = async (req, res) => { 10 | try { 11 | res.status(200).json(await getAllItemsFromDB()) 12 | } catch (error) { 13 | handleError(res, error) 14 | } 15 | } 16 | 17 | module.exports = { getAllCities } 18 | -------------------------------------------------------------------------------- /app/controllers/cities/getCities.js: -------------------------------------------------------------------------------- 1 | const City = require('../../models/city') 2 | const { checkQueryString, getItems } = require('../../middleware/db') 3 | const { handleError } = require('../../middleware/utils') 4 | 5 | /** 6 | * Get items function called by route 7 | * @param {Object} req - request object 8 | * @param {Object} res - response object 9 | */ 10 | const getCities = async (req, res) => { 11 | try { 12 | const query = await checkQueryString(req.query) 13 | res.status(200).json(await getItems(req, City, query)) 14 | } catch (error) { 15 | handleError(res, error) 16 | } 17 | } 18 | 19 | module.exports = { getCities } 20 | -------------------------------------------------------------------------------- /app/controllers/cities/getCity.js: -------------------------------------------------------------------------------- 1 | const { matchedData } = require('express-validator') 2 | const City = require('../../models/city') 3 | const { getItem } = require('../../middleware/db') 4 | const { isIDGood, handleError } = require('../../middleware/utils') 5 | 6 | /** 7 | * Get item function called by route 8 | * @param {Object} req - request object 9 | * @param {Object} res - response object 10 | */ 11 | const getCity = async (req, res) => { 12 | try { 13 | req = matchedData(req) 14 | const id = await isIDGood(req.id) 15 | res.status(200).json(await getItem(id, City)) 16 | } catch (error) { 17 | handleError(res, error) 18 | } 19 | } 20 | 21 | module.exports = { getCity } 22 | -------------------------------------------------------------------------------- /app/controllers/cities/helpers/cityExists.js: -------------------------------------------------------------------------------- 1 | const City = require('../../../models/city') 2 | const { buildErrObject } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Checks if a city already exists in database 6 | * @param {string} name - name of item 7 | */ 8 | const cityExists = (name = '') => { 9 | return new Promise((resolve, reject) => { 10 | City.findOne( 11 | { 12 | name 13 | }, 14 | (err, item) => { 15 | if (err) { 16 | return reject(buildErrObject(422, err.message)) 17 | } 18 | 19 | if (item) { 20 | return reject(buildErrObject(422, 'CITY_ALREADY_EXISTS')) 21 | } 22 | resolve(false) 23 | } 24 | ) 25 | }) 26 | } 27 | 28 | module.exports = { cityExists } 29 | -------------------------------------------------------------------------------- /app/controllers/cities/helpers/cityExistsExcludingItself.js: -------------------------------------------------------------------------------- 1 | const City = require('../../../models/city') 2 | const { buildErrObject } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Checks if a city already exists excluding itself 6 | * @param {string} id - id of item 7 | * @param {string} name - name of item 8 | */ 9 | const cityExistsExcludingItself = (id = '', name = '') => { 10 | return new Promise((resolve, reject) => { 11 | City.findOne( 12 | { 13 | name, 14 | _id: { 15 | $ne: id 16 | } 17 | }, 18 | (err, item) => { 19 | if (err) { 20 | return reject(buildErrObject(422, err.message)) 21 | } 22 | 23 | if (item) { 24 | return reject(buildErrObject(422, 'CITY_ALREADY_EXISTS')) 25 | } 26 | 27 | resolve(false) 28 | } 29 | ) 30 | }) 31 | } 32 | 33 | module.exports = { cityExistsExcludingItself } 34 | -------------------------------------------------------------------------------- /app/controllers/cities/helpers/getAllItemsFromDB.js: -------------------------------------------------------------------------------- 1 | const City = require('../../../models/city') 2 | const { buildErrObject } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Gets all items from database 6 | */ 7 | const getAllItemsFromDB = () => { 8 | return new Promise((resolve, reject) => { 9 | City.find( 10 | {}, 11 | '-updatedAt -createdAt', 12 | { 13 | sort: { 14 | name: 1 15 | } 16 | }, 17 | (err, items) => { 18 | if (err) { 19 | return reject(buildErrObject(422, err.message)) 20 | } 21 | resolve(items) 22 | } 23 | ) 24 | }) 25 | } 26 | 27 | module.exports = { getAllItemsFromDB } 28 | -------------------------------------------------------------------------------- /app/controllers/cities/helpers/index.js: -------------------------------------------------------------------------------- 1 | const { cityExists } = require('./cityExists') 2 | const { cityExistsExcludingItself } = require('./cityExistsExcludingItself') 3 | const { getAllItemsFromDB } = require('./getAllItemsFromDB') 4 | 5 | module.exports = { 6 | cityExists, 7 | cityExistsExcludingItself, 8 | getAllItemsFromDB 9 | } 10 | -------------------------------------------------------------------------------- /app/controllers/cities/index.js: -------------------------------------------------------------------------------- 1 | const { createCity } = require('./createCity') 2 | const { deleteCity } = require('./deleteCity') 3 | const { getAllCities } = require('./getAllCities') 4 | const { getCity } = require('./getCity') 5 | const { getCities } = require('./getCities') 6 | const { updateCity } = require('./updateCity') 7 | 8 | module.exports = { 9 | createCity, 10 | deleteCity, 11 | getAllCities, 12 | getCity, 13 | getCities, 14 | updateCity 15 | } 16 | -------------------------------------------------------------------------------- /app/controllers/cities/updateCity.js: -------------------------------------------------------------------------------- 1 | const City = require('../../models/city') 2 | const { updateItem } = require('../../middleware/db') 3 | const { isIDGood, handleError } = require('../../middleware/utils') 4 | const { matchedData } = require('express-validator') 5 | const { cityExistsExcludingItself } = require('./helpers') 6 | 7 | /** 8 | * Update item function called by route 9 | * @param {Object} req - request object 10 | * @param {Object} res - response object 11 | */ 12 | const updateCity = async (req, res) => { 13 | try { 14 | req = matchedData(req) 15 | const id = await isIDGood(req.id) 16 | const doesCityExists = await cityExistsExcludingItself(id, req.name) 17 | if (!doesCityExists) { 18 | res.status(200).json(await updateItem(id, City, req)) 19 | } 20 | } catch (error) { 21 | handleError(res, error) 22 | } 23 | } 24 | 25 | module.exports = { updateCity } 26 | -------------------------------------------------------------------------------- /app/controllers/cities/validators/index.js: -------------------------------------------------------------------------------- 1 | const { validateCreateCity } = require('./validateCreateCity') 2 | const { validateDeleteCity } = require('./validateDeleteCity') 3 | const { validateGetCity } = require('./validateGetCity') 4 | const { validateUpdateCity } = require('./validateUpdateCity') 5 | 6 | module.exports = { 7 | validateCreateCity, 8 | validateDeleteCity, 9 | validateGetCity, 10 | validateUpdateCity 11 | } 12 | -------------------------------------------------------------------------------- /app/controllers/cities/validators/validateCreateCity.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates create new item request 6 | */ 7 | const validateCreateCity = [ 8 | check('name') 9 | .exists() 10 | .withMessage('MISSING') 11 | .not() 12 | .isEmpty() 13 | .withMessage('IS_EMPTY') 14 | .trim(), 15 | (req, res, next) => { 16 | validateResult(req, res, next) 17 | } 18 | ] 19 | 20 | module.exports = { validateCreateCity } 21 | -------------------------------------------------------------------------------- /app/controllers/cities/validators/validateDeleteCity.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates delete item request 6 | */ 7 | const validateDeleteCity = [ 8 | check('id') 9 | .exists() 10 | .withMessage('MISSING') 11 | .not() 12 | .isEmpty() 13 | .withMessage('IS_EMPTY'), 14 | (req, res, next) => { 15 | validateResult(req, res, next) 16 | } 17 | ] 18 | 19 | module.exports = { validateDeleteCity } 20 | -------------------------------------------------------------------------------- /app/controllers/cities/validators/validateGetCity.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates get item request 6 | */ 7 | const validateGetCity = [ 8 | check('id') 9 | .exists() 10 | .withMessage('MISSING') 11 | .not() 12 | .isEmpty() 13 | .withMessage('IS_EMPTY'), 14 | (req, res, next) => { 15 | validateResult(req, res, next) 16 | } 17 | ] 18 | 19 | module.exports = { validateGetCity } 20 | -------------------------------------------------------------------------------- /app/controllers/cities/validators/validateUpdateCity.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates update item request 6 | */ 7 | const validateUpdateCity = [ 8 | check('name') 9 | .exists() 10 | .withMessage('MISSING') 11 | .not() 12 | .isEmpty() 13 | .withMessage('IS_EMPTY'), 14 | check('id') 15 | .exists() 16 | .withMessage('MISSING') 17 | .not() 18 | .isEmpty() 19 | .withMessage('IS_EMPTY'), 20 | (req, res, next) => { 21 | validateResult(req, res, next) 22 | } 23 | ] 24 | 25 | module.exports = { validateUpdateCity } 26 | -------------------------------------------------------------------------------- /app/controllers/profile/changePassword.js: -------------------------------------------------------------------------------- 1 | const { 2 | isIDGood, 3 | handleError, 4 | buildErrObject 5 | } = require('../../middleware/utils') 6 | const { matchedData } = require('express-validator') 7 | const { checkPassword } = require('../../middleware/auth') 8 | const { findUser, changePasswordInDB } = require('./helpers') 9 | 10 | /** 11 | * Change password function called by route 12 | * @param {Object} req - request object 13 | * @param {Object} res - response object 14 | */ 15 | const changePassword = async (req, res) => { 16 | try { 17 | const id = await isIDGood(req.user._id) 18 | const user = await findUser(id) 19 | req = matchedData(req) 20 | const isPasswordMatch = await checkPassword(req.oldPassword, user) 21 | if (!isPasswordMatch) { 22 | return handleError(res, buildErrObject(409, 'WRONG_PASSWORD')) 23 | } else { 24 | // all ok, proceed to change password 25 | res.status(200).json(await changePasswordInDB(id, req)) 26 | } 27 | } catch (error) { 28 | handleError(res, error) 29 | } 30 | } 31 | 32 | module.exports = { changePassword } 33 | -------------------------------------------------------------------------------- /app/controllers/profile/getProfile.js: -------------------------------------------------------------------------------- 1 | const { getProfileFromDB } = require('./helpers') 2 | const { isIDGood, handleError } = require('../../middleware/utils') 3 | 4 | /** 5 | * Get profile function called by route 6 | * @param {Object} req - request object 7 | * @param {Object} res - response object 8 | */ 9 | const getProfile = async (req, res) => { 10 | try { 11 | const id = await isIDGood(req.user._id) 12 | res.status(200).json(await getProfileFromDB(id)) 13 | } catch (error) { 14 | handleError(res, error) 15 | } 16 | } 17 | 18 | module.exports = { getProfile } 19 | -------------------------------------------------------------------------------- /app/controllers/profile/helpers/changePasswordInDB.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user') 2 | const { 3 | itemNotFound, 4 | buildErrObject, 5 | buildSuccObject 6 | } = require('../../../middleware/utils') 7 | 8 | /** 9 | * Changes password in database 10 | * @param {string} id - user id 11 | * @param {Object} req - request object 12 | */ 13 | const changePasswordInDB = (id = '', req = {}) => { 14 | return new Promise((resolve, reject) => { 15 | User.findById(id, '+password', async (err, user) => { 16 | try { 17 | await itemNotFound(err, user, 'NOT_FOUND') 18 | 19 | // Assigns new password to user 20 | user.password = req.newPassword 21 | 22 | // Saves in DB 23 | user.save((error) => { 24 | if (err) { 25 | return reject(buildErrObject(422, error.message)) 26 | } 27 | resolve(buildSuccObject('PASSWORD_CHANGED')) 28 | }) 29 | } catch (error) { 30 | reject(error) 31 | } 32 | }) 33 | }) 34 | } 35 | 36 | module.exports = { changePasswordInDB } 37 | -------------------------------------------------------------------------------- /app/controllers/profile/helpers/findUser.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user') 2 | const { itemNotFound } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Finds user by id 6 | * @param {string} id - user id 7 | */ 8 | const findUser = (id = '') => { 9 | return new Promise((resolve, reject) => { 10 | User.findById(id, 'password email', async (err, user) => { 11 | try { 12 | await itemNotFound(err, user, 'USER_DOES_NOT_EXIST') 13 | resolve(user) 14 | } catch (error) { 15 | reject(error) 16 | } 17 | }) 18 | }) 19 | } 20 | 21 | module.exports = { findUser } 22 | -------------------------------------------------------------------------------- /app/controllers/profile/helpers/getProfileFromDB.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user') 2 | const { itemNotFound } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Gets profile from database by id 6 | * @param {string} id - user id 7 | */ 8 | const getProfileFromDB = (id = '') => { 9 | return new Promise((resolve, reject) => { 10 | User.findById(id, '-_id -updatedAt -createdAt', async (err, user) => { 11 | try { 12 | await itemNotFound(err, user, 'NOT_FOUND') 13 | resolve(user) 14 | } catch (error) { 15 | reject(error) 16 | } 17 | }) 18 | }) 19 | } 20 | 21 | module.exports = { getProfileFromDB } 22 | -------------------------------------------------------------------------------- /app/controllers/profile/helpers/index.js: -------------------------------------------------------------------------------- 1 | const { changePasswordInDB } = require('./changePasswordInDB') 2 | const { findUser } = require('./findUser') 3 | const { getProfileFromDB } = require('./getProfileFromDB') 4 | const { updateProfileInDB } = require('./updateProfileInDB') 5 | 6 | module.exports = { 7 | changePasswordInDB, 8 | findUser, 9 | getProfileFromDB, 10 | updateProfileInDB 11 | } 12 | -------------------------------------------------------------------------------- /app/controllers/profile/helpers/updateProfileInDB.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user') 2 | const { itemNotFound } = require('../../../middleware/utils') 3 | 4 | /** 5 | * Updates profile in database 6 | * @param {Object} req - request object 7 | * @param {string} id - user id 8 | */ 9 | const updateProfileInDB = (req = {}, id = '') => { 10 | return new Promise((resolve, reject) => { 11 | User.findByIdAndUpdate( 12 | id, 13 | req, 14 | { 15 | new: true, 16 | runValidators: true, 17 | select: '-role -_id -updatedAt -createdAt' 18 | }, 19 | async (err, user) => { 20 | try { 21 | await itemNotFound(err, user, 'NOT_FOUND') 22 | resolve(user) 23 | } catch (error) { 24 | reject(error) 25 | } 26 | } 27 | ) 28 | }) 29 | } 30 | 31 | module.exports = { updateProfileInDB } 32 | -------------------------------------------------------------------------------- /app/controllers/profile/index.js: -------------------------------------------------------------------------------- 1 | const { changePassword } = require('./changePassword') 2 | const { getProfile } = require('./getProfile') 3 | const { updateProfile } = require('./updateProfile') 4 | 5 | module.exports = { 6 | changePassword, 7 | getProfile, 8 | updateProfile 9 | } 10 | -------------------------------------------------------------------------------- /app/controllers/profile/updateProfile.js: -------------------------------------------------------------------------------- 1 | const { isIDGood, handleError } = require('../../middleware/utils') 2 | const { matchedData } = require('express-validator') 3 | const { updateProfileInDB } = require('./helpers') 4 | 5 | /** 6 | * Update profile function called by route 7 | * @param {Object} req - request object 8 | * @param {Object} res - response object 9 | */ 10 | const updateProfile = async (req, res) => { 11 | try { 12 | const id = await isIDGood(req.user._id) 13 | req = matchedData(req) 14 | res.status(200).json(await updateProfileInDB(req, id)) 15 | } catch (error) { 16 | handleError(res, error) 17 | } 18 | } 19 | 20 | module.exports = { updateProfile } 21 | -------------------------------------------------------------------------------- /app/controllers/profile/validators/index.js: -------------------------------------------------------------------------------- 1 | const { validateChangePassword } = require('./validateChangePassword') 2 | const { validateUpdateProfile } = require('./validateUpdateProfile') 3 | 4 | module.exports = { 5 | validateChangePassword, 6 | validateUpdateProfile 7 | } 8 | -------------------------------------------------------------------------------- /app/controllers/profile/validators/validateChangePassword.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates change password request 6 | */ 7 | const validateChangePassword = [ 8 | check('oldPassword') 9 | .optional() 10 | .not() 11 | .isEmpty() 12 | .withMessage('IS_EMPTY') 13 | .isLength({ 14 | min: 5 15 | }) 16 | .withMessage('PASSWORD_TOO_SHORT_MIN_5'), 17 | check('newPassword') 18 | .optional() 19 | .not() 20 | .isEmpty() 21 | .withMessage('IS_EMPTY') 22 | .isLength({ 23 | min: 5 24 | }) 25 | .withMessage('PASSWORD_TOO_SHORT_MIN_5'), 26 | (req, res, next) => { 27 | validateResult(req, res, next) 28 | } 29 | ] 30 | 31 | module.exports = { validateChangePassword } 32 | -------------------------------------------------------------------------------- /app/controllers/profile/validators/validateUpdateProfile.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const validator = require('validator') 3 | const { check } = require('express-validator') 4 | 5 | /** 6 | * Validates update profile request 7 | */ 8 | const validateUpdateProfile = [ 9 | check('name') 10 | .exists() 11 | .withMessage('MISSING') 12 | .not() 13 | .isEmpty() 14 | .withMessage('IS_EMPTY'), 15 | check('phone') 16 | .exists() 17 | .withMessage('MISSING') 18 | .not() 19 | .isEmpty() 20 | .withMessage('IS_EMPTY') 21 | .trim(), 22 | check('city') 23 | .exists() 24 | .withMessage('MISSING') 25 | .not() 26 | .isEmpty() 27 | .withMessage('IS_EMPTY') 28 | .trim(), 29 | check('country') 30 | .exists() 31 | .withMessage('MISSING') 32 | .not() 33 | .isEmpty() 34 | .withMessage('IS_EMPTY') 35 | .trim(), 36 | check('urlTwitter') 37 | .optional() 38 | .custom((v) => (v === '' ? true : validator.isURL(v))) 39 | .withMessage('NOT_A_VALID_URL'), 40 | check('urlGitHub') 41 | .optional() 42 | .custom((v) => (v === '' ? true : validator.isURL(v))) 43 | .withMessage('NOT_A_VALID_URL'), 44 | (req, res, next) => { 45 | validateResult(req, res, next) 46 | } 47 | ] 48 | 49 | module.exports = { validateUpdateProfile } 50 | -------------------------------------------------------------------------------- /app/controllers/users/createUser.js: -------------------------------------------------------------------------------- 1 | const { matchedData } = require('express-validator') 2 | const { handleError } = require('../../middleware/utils') 3 | const { 4 | emailExists, 5 | sendRegistrationEmailMessage 6 | } = require('../../middleware/emailer') 7 | const { createItemInDb } = require('./helpers') 8 | 9 | /** 10 | * Create item function called by route 11 | * @param {Object} req - request object 12 | * @param {Object} res - response object 13 | */ 14 | const createUser = async (req, res) => { 15 | try { 16 | // Gets locale from header 'Accept-Language' 17 | const locale = req.getLocale() 18 | req = matchedData(req) 19 | const doesEmailExists = await emailExists(req.email) 20 | if (!doesEmailExists) { 21 | const item = await createItemInDb(req) 22 | sendRegistrationEmailMessage(locale, item) 23 | res.status(201).json(item) 24 | } 25 | } catch (error) { 26 | handleError(res, error) 27 | } 28 | } 29 | 30 | module.exports = { createUser } 31 | -------------------------------------------------------------------------------- /app/controllers/users/deleteUser.js: -------------------------------------------------------------------------------- 1 | const model = require('../../models/user') 2 | const { matchedData } = require('express-validator') 3 | const { isIDGood, handleError } = require('../../middleware/utils') 4 | const { deleteItem } = require('../../middleware/db') 5 | 6 | /** 7 | * Delete item function called by route 8 | * @param {Object} req - request object 9 | * @param {Object} res - response object 10 | */ 11 | const deleteUser = async (req, res) => { 12 | try { 13 | req = matchedData(req) 14 | const id = await isIDGood(req.id) 15 | res.status(200).json(await deleteItem(id, model)) 16 | } catch (error) { 17 | handleError(res, error) 18 | } 19 | } 20 | 21 | module.exports = { deleteUser } 22 | -------------------------------------------------------------------------------- /app/controllers/users/getUser.js: -------------------------------------------------------------------------------- 1 | const User = require('../../models/user') 2 | const { matchedData } = require('express-validator') 3 | const { isIDGood, handleError } = require('../../middleware/utils') 4 | const { getItem } = require('../../middleware/db') 5 | 6 | /** 7 | * Get item function called by route 8 | * @param {Object} req - request object 9 | * @param {Object} res - response object 10 | */ 11 | const getUser = async (req, res) => { 12 | try { 13 | req = matchedData(req) 14 | const id = await isIDGood(req.id) 15 | res.status(200).json(await getItem(id, User)) 16 | } catch (error) { 17 | handleError(res, error) 18 | } 19 | } 20 | 21 | module.exports = { getUser } 22 | -------------------------------------------------------------------------------- /app/controllers/users/getUsers.js: -------------------------------------------------------------------------------- 1 | const User = require('../../models/user') 2 | const { handleError } = require('../../middleware/utils') 3 | const { getItems, checkQueryString } = require('../../middleware/db') 4 | 5 | /** 6 | * Get items function called by route 7 | * @param {Object} req - request object 8 | * @param {Object} res - response object 9 | */ 10 | const getUsers = async (req, res) => { 11 | try { 12 | const query = await checkQueryString(req.query) 13 | res.status(200).json(await getItems(req, User, query)) 14 | } catch (error) { 15 | handleError(res, error) 16 | } 17 | } 18 | 19 | module.exports = { getUsers } 20 | -------------------------------------------------------------------------------- /app/controllers/users/helpers/createItemInDb.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid') 2 | const User = require('../../../models/user') 3 | const { buildErrObject } = require('../../../middleware/utils') 4 | 5 | /** 6 | * Creates a new item in database 7 | * @param {Object} req - request object 8 | */ 9 | const createItemInDb = ({ 10 | name = '', 11 | email = '', 12 | password = '', 13 | role = '', 14 | phone = '', 15 | city = '', 16 | country = '' 17 | }) => { 18 | return new Promise((resolve, reject) => { 19 | const user = new User({ 20 | name, 21 | email, 22 | password, 23 | role, 24 | phone, 25 | city, 26 | country, 27 | verification: uuid.v4() 28 | }) 29 | user.save((err, item) => { 30 | if (err) { 31 | reject(buildErrObject(422, err.message)) 32 | } 33 | 34 | item = JSON.parse(JSON.stringify(item)) 35 | 36 | delete item.password 37 | delete item.blockExpires 38 | delete item.loginAttempts 39 | 40 | resolve(item) 41 | }) 42 | }) 43 | } 44 | 45 | module.exports = { createItemInDb } 46 | -------------------------------------------------------------------------------- /app/controllers/users/helpers/index.js: -------------------------------------------------------------------------------- 1 | const { createItemInDb } = require('./createItemInDb') 2 | 3 | module.exports = { 4 | createItemInDb 5 | } 6 | -------------------------------------------------------------------------------- /app/controllers/users/index.js: -------------------------------------------------------------------------------- 1 | const { createUser } = require('./createUser') 2 | const { deleteUser } = require('./deleteUser') 3 | const { getUser } = require('./getUser') 4 | const { getUsers } = require('./getUsers') 5 | const { updateUser } = require('./updateUser') 6 | 7 | module.exports = { 8 | createUser, 9 | deleteUser, 10 | getUser, 11 | getUsers, 12 | updateUser 13 | } 14 | -------------------------------------------------------------------------------- /app/controllers/users/updateUser.js: -------------------------------------------------------------------------------- 1 | const User = require('../../models/user') 2 | const { matchedData } = require('express-validator') 3 | const { isIDGood, handleError } = require('../../middleware/utils') 4 | const { updateItem } = require('../../middleware/db') 5 | const { emailExistsExcludingMyself } = require('../../middleware/emailer') 6 | 7 | /** 8 | * Update item function called by route 9 | * @param {Object} req - request object 10 | * @param {Object} res - response object 11 | */ 12 | const updateUser = async (req, res) => { 13 | try { 14 | req = matchedData(req) 15 | const id = await isIDGood(req.id) 16 | const doesEmailExists = await emailExistsExcludingMyself(id, req.email) 17 | if (!doesEmailExists) { 18 | res.status(200).json(await updateItem(id, User, req)) 19 | } 20 | } catch (error) { 21 | handleError(res, error) 22 | } 23 | } 24 | 25 | module.exports = { updateUser } 26 | -------------------------------------------------------------------------------- /app/controllers/users/validators/index.js: -------------------------------------------------------------------------------- 1 | const { validateCreateUser } = require('./validateCreateUser') 2 | const { validateDeleteUser } = require('./validateDeleteUser') 3 | const { validateGetUser } = require('./validateGetUser') 4 | const { validateUpdateUser } = require('./validateUpdateUser') 5 | 6 | module.exports = { 7 | validateCreateUser, 8 | validateDeleteUser, 9 | validateGetUser, 10 | validateUpdateUser 11 | } 12 | -------------------------------------------------------------------------------- /app/controllers/users/validators/validateCreateUser.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const validator = require('validator') 3 | const { check } = require('express-validator') 4 | 5 | /** 6 | * Validates create new item request 7 | */ 8 | const validateCreateUser = [ 9 | check('name') 10 | .exists() 11 | .withMessage('MISSING') 12 | .not() 13 | .isEmpty() 14 | .withMessage('IS_EMPTY'), 15 | check('email') 16 | .exists() 17 | .withMessage('MISSING') 18 | .not() 19 | .isEmpty() 20 | .withMessage('IS_EMPTY') 21 | .isEmail() 22 | .withMessage('EMAIL_IS_NOT_VALID'), 23 | check('password') 24 | .exists() 25 | .withMessage('MISSING') 26 | .not() 27 | .isEmpty() 28 | .withMessage('IS_EMPTY') 29 | .isLength({ 30 | min: 5 31 | }) 32 | .withMessage('PASSWORD_TOO_SHORT_MIN_5'), 33 | check('role') 34 | .exists() 35 | .withMessage('MISSING') 36 | .not() 37 | .isEmpty() 38 | .withMessage('IS_EMPTY') 39 | .isIn(['user', 'admin']) 40 | .withMessage('USER_NOT_IN_KNOWN_ROLE'), 41 | check('phone') 42 | .exists() 43 | .withMessage('MISSING') 44 | .not() 45 | .isEmpty() 46 | .withMessage('IS_EMPTY') 47 | .trim(), 48 | check('city') 49 | .exists() 50 | .withMessage('MISSING') 51 | .not() 52 | .isEmpty() 53 | .withMessage('IS_EMPTY') 54 | .trim(), 55 | check('country') 56 | .exists() 57 | .withMessage('MISSING') 58 | .not() 59 | .isEmpty() 60 | .withMessage('IS_EMPTY') 61 | .trim(), 62 | check('urlTwitter') 63 | .optional() 64 | .custom((v) => (v === '' ? true : validator.isURL(v))) 65 | .withMessage('NOT_A_VALID_URL'), 66 | check('urlGitHub') 67 | .optional() 68 | .custom((v) => (v === '' ? true : validator.isURL(v))) 69 | .withMessage('NOT_A_VALID_URL'), 70 | (req, res, next) => { 71 | validateResult(req, res, next) 72 | } 73 | ] 74 | 75 | module.exports = { validateCreateUser } 76 | -------------------------------------------------------------------------------- /app/controllers/users/validators/validateDeleteUser.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates delete item request 6 | */ 7 | const validateDeleteUser = [ 8 | check('id') 9 | .exists() 10 | .withMessage('MISSING') 11 | .not() 12 | .isEmpty() 13 | .withMessage('IS_EMPTY'), 14 | (req, res, next) => { 15 | validateResult(req, res, next) 16 | } 17 | ] 18 | 19 | module.exports = { validateDeleteUser } 20 | -------------------------------------------------------------------------------- /app/controllers/users/validators/validateGetUser.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const { check } = require('express-validator') 3 | 4 | /** 5 | * Validates get item request 6 | */ 7 | const validateGetUser = [ 8 | check('id') 9 | .exists() 10 | .withMessage('MISSING') 11 | .not() 12 | .isEmpty() 13 | .withMessage('IS_EMPTY'), 14 | (req, res, next) => { 15 | validateResult(req, res, next) 16 | } 17 | ] 18 | 19 | module.exports = { validateGetUser } 20 | -------------------------------------------------------------------------------- /app/controllers/users/validators/validateUpdateUser.js: -------------------------------------------------------------------------------- 1 | const { validateResult } = require('../../../middleware/utils') 2 | const validator = require('validator') 3 | const { check } = require('express-validator') 4 | 5 | /** 6 | * Validates update item request 7 | */ 8 | const validateUpdateUser = [ 9 | check('name') 10 | .exists() 11 | .withMessage('MISSING') 12 | .not() 13 | .isEmpty() 14 | .withMessage('IS_EMPTY'), 15 | check('email') 16 | .exists() 17 | .withMessage('MISSING') 18 | .not() 19 | .isEmpty() 20 | .withMessage('IS_EMPTY'), 21 | check('role') 22 | .exists() 23 | .withMessage('MISSING') 24 | .not() 25 | .isEmpty() 26 | .withMessage('IS_EMPTY'), 27 | check('phone') 28 | .exists() 29 | .withMessage('MISSING') 30 | .not() 31 | .isEmpty() 32 | .withMessage('IS_EMPTY') 33 | .trim(), 34 | check('city') 35 | .exists() 36 | .withMessage('MISSING') 37 | .not() 38 | .isEmpty() 39 | .withMessage('IS_EMPTY') 40 | .trim(), 41 | check('country') 42 | .exists() 43 | .withMessage('MISSING') 44 | .not() 45 | .isEmpty() 46 | .withMessage('IS_EMPTY') 47 | .trim(), 48 | check('urlTwitter') 49 | .optional() 50 | .custom((v) => (v === '' ? true : validator.isURL(v))) 51 | .withMessage('NOT_A_VALID_URL'), 52 | check('urlGitHub') 53 | .optional() 54 | .custom((v) => (v === '' ? true : validator.isURL(v))) 55 | .withMessage('NOT_A_VALID_URL'), 56 | check('id') 57 | .exists() 58 | .withMessage('MISSING') 59 | .not() 60 | .isEmpty() 61 | .withMessage('IS_EMPTY'), 62 | (req, res, next) => { 63 | validateResult(req, res, next) 64 | } 65 | ] 66 | 67 | module.exports = { validateUpdateUser } 68 | -------------------------------------------------------------------------------- /app/middleware/auth/checkPassword.js: -------------------------------------------------------------------------------- 1 | const { buildErrObject } = require('../../middleware/utils') 2 | 3 | /** 4 | * Checks is password matches 5 | * @param {string} password - password 6 | * @param {Object} user - user object 7 | * @returns {boolean} 8 | */ 9 | const checkPassword = (password = '', user = {}) => { 10 | return new Promise((resolve, reject) => { 11 | user.comparePassword(password, (err, isMatch) => { 12 | if (err) { 13 | return reject(buildErrObject(422, err.message)) 14 | } 15 | if (!isMatch) { 16 | resolve(false) 17 | } 18 | resolve(true) 19 | }) 20 | }) 21 | } 22 | 23 | module.exports = { checkPassword } 24 | -------------------------------------------------------------------------------- /app/middleware/auth/decrypt.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | const secret = process.env.JWT_SECRET 4 | const algorithm = 'aes-256-cbc' 5 | // Key length is dependent on the algorithm. In this case for aes256, it is 6 | // 32 bytes (256 bits). 7 | const key = crypto.scryptSync(secret, 'salt', 32) 8 | const iv = Buffer.alloc(16, 0) // Initialization crypto vector 9 | 10 | /** 11 | * Decrypts text 12 | * @param {string} text - text to decrypt 13 | */ 14 | const decrypt = (text = '') => { 15 | const decipher = crypto.createDecipheriv(algorithm, key, iv) 16 | 17 | try { 18 | let decrypted = decipher.update(text, 'hex', 'utf8') 19 | decrypted += decipher.final('utf8') 20 | return decrypted 21 | } catch (err) { 22 | return err 23 | } 24 | } 25 | 26 | module.exports = { decrypt } 27 | -------------------------------------------------------------------------------- /app/middleware/auth/encrypt.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | const secret = process.env.JWT_SECRET 4 | const algorithm = 'aes-256-cbc' 5 | // Key length is dependent on the algorithm. In this case for aes256, it is 6 | // 32 bytes (256 bits). 7 | const key = crypto.scryptSync(secret, 'salt', 32) 8 | const iv = Buffer.alloc(16, 0) // Initialization crypto vector 9 | 10 | /** 11 | * Encrypts text 12 | * @param {string} text - text to encrypt 13 | */ 14 | const encrypt = (text = '') => { 15 | const cipher = crypto.createCipheriv(algorithm, key, iv) 16 | 17 | let encrypted = cipher.update(text, 'utf8', 'hex') 18 | encrypted += cipher.final('hex') 19 | 20 | return encrypted 21 | } 22 | 23 | module.exports = { encrypt } 24 | -------------------------------------------------------------------------------- /app/middleware/auth/index.js: -------------------------------------------------------------------------------- 1 | const { checkPassword } = require('./checkPassword') 2 | const { decrypt } = require('./decrypt') 3 | const { encrypt } = require('./encrypt') 4 | 5 | module.exports = { 6 | checkPassword, 7 | decrypt, 8 | encrypt 9 | } 10 | -------------------------------------------------------------------------------- /app/middleware/db/buildSort.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds sorting 3 | * @param {string} sort - field to sort from 4 | * @param {number} order - order for query (1,-1) 5 | */ 6 | const buildSort = (sort = '', order = 1) => { 7 | const sortBy = {} 8 | sortBy[sort] = order 9 | return sortBy 10 | } 11 | 12 | module.exports = { buildSort } 13 | -------------------------------------------------------------------------------- /app/middleware/db/checkQueryString.js: -------------------------------------------------------------------------------- 1 | const { buildErrObject } = require('../../middleware/utils') 2 | 3 | /** 4 | * Checks the query string for filtering records 5 | * query.filter should be the text to search (string) 6 | * query.fields should be the fields to search into (array) 7 | * @param {Object} query - query object 8 | */ 9 | const checkQueryString = (query = {}) => { 10 | return new Promise((resolve, reject) => { 11 | try { 12 | if ( 13 | typeof query.filter !== 'undefined' && 14 | typeof query.fields !== 'undefined' 15 | ) { 16 | const data = { 17 | $or: [] 18 | } 19 | const array = [] 20 | // Takes fields param and builds an array by splitting with ',' 21 | const arrayFields = query.fields.split(',') 22 | // Adds SQL Like %word% with regex 23 | arrayFields.map((item) => { 24 | array.push({ 25 | [item]: { 26 | $regex: new RegExp(query.filter, 'i') 27 | } 28 | }) 29 | }) 30 | // Puts array result in data 31 | data.$or = array 32 | resolve(data) 33 | } else { 34 | resolve({}) 35 | } 36 | } catch (err) { 37 | console.log(err.message) 38 | reject(buildErrObject(422, 'ERROR_WITH_FILTER')) 39 | } 40 | }) 41 | } 42 | 43 | module.exports = { checkQueryString } 44 | -------------------------------------------------------------------------------- /app/middleware/db/cleanPaginationID.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hack for mongoose-paginate, removes 'id' from results 3 | * @param {Object} result - result object 4 | */ 5 | const cleanPaginationID = (result = {}) => { 6 | result.docs.map((element) => delete element.id) 7 | return result 8 | } 9 | 10 | module.exports = { cleanPaginationID } 11 | -------------------------------------------------------------------------------- /app/middleware/db/createItem.js: -------------------------------------------------------------------------------- 1 | const { buildErrObject } = require('../../middleware/utils') 2 | 3 | /** 4 | * Creates a new item in database 5 | * @param {Object} req - request object 6 | */ 7 | const createItem = (req = {}, model = {}) => { 8 | return new Promise((resolve, reject) => { 9 | model.create(req, (err, item) => { 10 | if (err) { 11 | reject(buildErrObject(422, err.message)) 12 | } 13 | resolve(item) 14 | }) 15 | }) 16 | } 17 | 18 | module.exports = { createItem } 19 | -------------------------------------------------------------------------------- /app/middleware/db/deleteItem.js: -------------------------------------------------------------------------------- 1 | const { buildSuccObject, itemNotFound } = require('../../middleware/utils') 2 | 3 | /** 4 | * Deletes an item from database by id 5 | * @param {string} id - id of item 6 | */ 7 | const deleteItem = (id = '', model = {}) => { 8 | return new Promise((resolve, reject) => { 9 | model.findByIdAndRemove(id, async (err, item) => { 10 | try { 11 | await itemNotFound(err, item, 'NOT_FOUND') 12 | resolve(buildSuccObject('DELETED')) 13 | } catch (error) { 14 | reject(error) 15 | } 16 | }) 17 | }) 18 | } 19 | 20 | module.exports = { deleteItem } 21 | -------------------------------------------------------------------------------- /app/middleware/db/getItem.js: -------------------------------------------------------------------------------- 1 | const { itemNotFound } = require('../../middleware/utils') 2 | 3 | /** 4 | * Gets item from database by id 5 | * @param {string} id - item id 6 | */ 7 | const getItem = (id = '', model = {}) => { 8 | return new Promise((resolve, reject) => { 9 | model.findById(id, async (err, item) => { 10 | try { 11 | await itemNotFound(err, item, 'NOT_FOUND') 12 | resolve(item) 13 | } catch (error) { 14 | reject(error) 15 | } 16 | }) 17 | }) 18 | } 19 | 20 | module.exports = { getItem } 21 | -------------------------------------------------------------------------------- /app/middleware/db/getItems.js: -------------------------------------------------------------------------------- 1 | const { buildErrObject } = require('../../middleware/utils') 2 | 3 | const { listInitOptions } = require('./listInitOptions') 4 | const { cleanPaginationID } = require('./cleanPaginationID') 5 | 6 | /** 7 | * Gets items from database 8 | * @param {Object} req - request object 9 | * @param {Object} query - query object 10 | */ 11 | const getItems = async (req = {}, model = {}, query = {}) => { 12 | const options = await listInitOptions(req) 13 | return new Promise((resolve, reject) => { 14 | model.paginate(query, options, (err, items) => { 15 | if (err) { 16 | return reject(buildErrObject(422, err.message)) 17 | } 18 | resolve(cleanPaginationID(items)) 19 | }) 20 | }) 21 | } 22 | 23 | module.exports = { getItems } 24 | -------------------------------------------------------------------------------- /app/middleware/db/index.js: -------------------------------------------------------------------------------- 1 | const { buildSort } = require('./buildSort') 2 | const { checkQueryString } = require('./checkQueryString') 3 | const { cleanPaginationID } = require('./cleanPaginationID') 4 | const { createItem } = require('./createItem') 5 | const { deleteItem } = require('./deleteItem') 6 | const { getItem } = require('./getItem') 7 | const { getItems } = require('./getItems') 8 | const { listInitOptions } = require('./listInitOptions') 9 | const { updateItem } = require('./updateItem') 10 | 11 | module.exports = { 12 | buildSort, 13 | checkQueryString, 14 | cleanPaginationID, 15 | createItem, 16 | deleteItem, 17 | getItem, 18 | getItems, 19 | listInitOptions, 20 | updateItem 21 | } 22 | -------------------------------------------------------------------------------- /app/middleware/db/listInitOptions.js: -------------------------------------------------------------------------------- 1 | const { buildErrObject } = require('../../middleware/utils') 2 | const { buildSort } = require('./buildSort') 3 | 4 | /** 5 | * Builds initial options for query 6 | * @param {Object} query - query object 7 | */ 8 | const listInitOptions = (req = {}) => { 9 | return new Promise(async (resolve, reject) => { 10 | try { 11 | const order = req.query.order || -1 12 | const sort = req.query.sort || 'createdAt' 13 | const sortBy = buildSort(sort, order) 14 | const page = parseInt(req.query.page, 10) || 1 15 | const limit = parseInt(req.query.limit, 10) || 5 16 | const options = { 17 | sort: sortBy, 18 | lean: true, 19 | page, 20 | limit 21 | } 22 | resolve(options) 23 | } catch (error) { 24 | console.log(error.message) 25 | reject(buildErrObject(422, 'ERROR_WITH_INIT_OPTIONS')) 26 | } 27 | }) 28 | } 29 | 30 | module.exports = { listInitOptions } 31 | -------------------------------------------------------------------------------- /app/middleware/db/updateItem.js: -------------------------------------------------------------------------------- 1 | const { itemNotFound } = require('../../middleware/utils') 2 | 3 | /** 4 | * Updates an item in database by id 5 | * @param {string} id - item id 6 | * @param {Object} req - request object 7 | */ 8 | const updateItem = (id = '', model = {}, req = {}) => { 9 | return new Promise((resolve, reject) => { 10 | model.findByIdAndUpdate( 11 | id, 12 | req, 13 | { 14 | new: true, 15 | runValidators: true 16 | }, 17 | async (err, item) => { 18 | try { 19 | await itemNotFound(err, item, 'NOT_FOUND') 20 | resolve(item) 21 | } catch (error) { 22 | reject(error) 23 | } 24 | } 25 | ) 26 | }) 27 | } 28 | 29 | module.exports = { updateItem } 30 | -------------------------------------------------------------------------------- /app/middleware/emailer/emailExists.js: -------------------------------------------------------------------------------- 1 | const User = require('../../models/user') 2 | const { buildErrObject } = require('../../middleware/utils') 3 | 4 | /** 5 | * Checks User model if user with an specific email exists 6 | * @param {string} email - user email 7 | */ 8 | const emailExists = (email = '') => { 9 | return new Promise((resolve, reject) => { 10 | User.findOne( 11 | { 12 | email 13 | }, 14 | (err, item) => { 15 | if (err) { 16 | return reject(buildErrObject(422, err.message)) 17 | } 18 | 19 | if (item) { 20 | return reject(buildErrObject(422, 'EMAIL_ALREADY_EXISTS')) 21 | } 22 | resolve(false) 23 | } 24 | ) 25 | }) 26 | } 27 | 28 | module.exports = { emailExists } 29 | -------------------------------------------------------------------------------- /app/middleware/emailer/emailExistsExcludingMyself.js: -------------------------------------------------------------------------------- 1 | const User = require('../../models/user') 2 | const { buildErrObject } = require('../../middleware/utils') 3 | 4 | /** 5 | * Checks User model if user with an specific email exists but excluding user id 6 | * @param {string} id - user id 7 | * @param {string} email - user email 8 | */ 9 | const emailExistsExcludingMyself = (id = '', email = '') => { 10 | return new Promise((resolve, reject) => { 11 | User.findOne( 12 | { 13 | email, 14 | _id: { 15 | $ne: id 16 | } 17 | }, 18 | async (err, item) => { 19 | if (err) { 20 | return reject(buildErrObject(422, err.message)) 21 | } 22 | 23 | if (item) { 24 | return reject(buildErrObject(422, 'EMAIL_ALREADY_EXISTS')) 25 | } 26 | 27 | resolve(false) 28 | } 29 | ) 30 | }) 31 | } 32 | 33 | module.exports = { emailExistsExcludingMyself } 34 | -------------------------------------------------------------------------------- /app/middleware/emailer/index.js: -------------------------------------------------------------------------------- 1 | const { emailExists } = require('./emailExists') 2 | const { emailExistsExcludingMyself } = require('./emailExistsExcludingMyself') 3 | const { prepareToSendEmail } = require('./prepareToSendEmail') 4 | const { sendEmail } = require('./sendEmail') 5 | const { 6 | sendRegistrationEmailMessage 7 | } = require('./sendRegistrationEmailMessage') 8 | const { 9 | sendResetPasswordEmailMessage 10 | } = require('./sendResetPasswordEmailMessage') 11 | 12 | module.exports = { 13 | emailExists, 14 | emailExistsExcludingMyself, 15 | prepareToSendEmail, 16 | sendEmail, 17 | sendRegistrationEmailMessage, 18 | sendResetPasswordEmailMessage 19 | } 20 | -------------------------------------------------------------------------------- /app/middleware/emailer/prepareToSendEmail.js: -------------------------------------------------------------------------------- 1 | const { sendEmail } = require('./sendEmail') 2 | 3 | /** 4 | * Prepares to send email 5 | * @param {string} user - user object 6 | * @param {string} subject - subject 7 | * @param {string} htmlMessage - html message 8 | */ 9 | const prepareToSendEmail = (user = {}, subject = '', htmlMessage = '') => { 10 | user = { 11 | name: user.name, 12 | email: user.email, 13 | verification: user.verification 14 | } 15 | const data = { 16 | user, 17 | subject, 18 | htmlMessage 19 | } 20 | if (process.env.NODE_ENV === 'production') { 21 | sendEmail(data, (messageSent) => 22 | messageSent 23 | ? console.log(`Email SENT to: ${user.email}`) 24 | : console.log(`Email FAILED to: ${user.email}`) 25 | ) 26 | } else if (process.env.NODE_ENV === 'development') { 27 | console.log(data) 28 | } 29 | } 30 | 31 | module.exports = { prepareToSendEmail } 32 | -------------------------------------------------------------------------------- /app/middleware/emailer/sendEmail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer') 2 | const mg = require('nodemailer-mailgun-transport') 3 | 4 | /** 5 | * Sends email 6 | * @param {Object} data - data 7 | * @param {boolean} callback - callback 8 | */ 9 | const sendEmail = async (data = {}, callback) => { 10 | const auth = { 11 | auth: { 12 | // eslint-disable-next-line camelcase 13 | api_key: process.env.EMAIL_SMTP_API_MAILGUN, 14 | domain: process.env.EMAIL_SMTP_DOMAIN_MAILGUN 15 | } 16 | // host: 'api.eu.mailgun.net' // THIS IS NEEDED WHEN USING EUROPEAN SERVERS 17 | } 18 | const transporter = nodemailer.createTransport(mg(auth)) 19 | const mailOptions = { 20 | from: `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`, 21 | to: `${data.user.name} <${data.user.email}>`, 22 | subject: data.subject, 23 | html: data.htmlMessage 24 | } 25 | transporter.sendMail(mailOptions, (err) => { 26 | if (err) { 27 | return callback(false) 28 | } 29 | return callback(true) 30 | }) 31 | } 32 | 33 | module.exports = { sendEmail } 34 | -------------------------------------------------------------------------------- /app/middleware/emailer/sendRegistrationEmailMessage.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | const { prepareToSendEmail } = require('./prepareToSendEmail') 3 | 4 | /** 5 | * Sends registration email 6 | * @param {string} locale - locale 7 | * @param {Object} user - user object 8 | */ 9 | const sendRegistrationEmailMessage = (locale = '', user = {}) => { 10 | i18n.setLocale(locale) 11 | const subject = i18n.__('registration.SUBJECT') 12 | const htmlMessage = i18n.__( 13 | 'registration.MESSAGE', 14 | user.name, 15 | process.env.FRONTEND_URL, 16 | user.verification 17 | ) 18 | prepareToSendEmail(user, subject, htmlMessage) 19 | } 20 | 21 | module.exports = { sendRegistrationEmailMessage } 22 | -------------------------------------------------------------------------------- /app/middleware/emailer/sendResetPasswordEmailMessage.js: -------------------------------------------------------------------------------- 1 | const i18n = require('i18n') 2 | const { prepareToSendEmail } = require('./prepareToSendEmail') 3 | 4 | /** 5 | * Sends reset password email 6 | * @param {string} locale - locale 7 | * @param {Object} user - user object 8 | */ 9 | const sendResetPasswordEmailMessage = (locale = '', user = {}) => { 10 | i18n.setLocale(locale) 11 | const subject = i18n.__('forgotPassword.SUBJECT') 12 | const htmlMessage = i18n.__( 13 | 'forgotPassword.MESSAGE', 14 | user.email, 15 | process.env.FRONTEND_URL, 16 | user.verification 17 | ) 18 | prepareToSendEmail(user, subject, htmlMessage) 19 | } 20 | 21 | module.exports = { sendResetPasswordEmailMessage } 22 | -------------------------------------------------------------------------------- /app/middleware/utils/buildErrObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds error object 3 | * @param {number} code - error code 4 | * @param {string} message - error text 5 | */ 6 | const buildErrObject = (code = '', message = '') => { 7 | return { 8 | code, 9 | message 10 | } 11 | } 12 | 13 | module.exports = { buildErrObject } 14 | -------------------------------------------------------------------------------- /app/middleware/utils/buildSuccObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds success object 3 | * @param {string} message - success text 4 | */ 5 | const buildSuccObject = (message = '') => { 6 | return { 7 | msg: message 8 | } 9 | } 10 | 11 | module.exports = { buildSuccObject } 12 | -------------------------------------------------------------------------------- /app/middleware/utils/getBrowserInfo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets browser info from user 3 | * @param {*} req - request object 4 | */ 5 | const getBrowserInfo = ({ headers }) => headers['user-agent'] 6 | 7 | module.exports = { getBrowserInfo } 8 | -------------------------------------------------------------------------------- /app/middleware/utils/getCountry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets country from user using CloudFlare header 'cf-ipcountry' 3 | * @param {*} req - request object 4 | */ 5 | const getCountry = ({ headers }) => 6 | headers['cf-ipcountry'] ? headers['cf-ipcountry'] : 'XX' 7 | 8 | module.exports = { getCountry } 9 | -------------------------------------------------------------------------------- /app/middleware/utils/getIP.js: -------------------------------------------------------------------------------- 1 | const requestIp = require('request-ip') 2 | 3 | /** 4 | * Gets IP from user 5 | * @param {*} req - request object 6 | */ 7 | const getIP = (req) => requestIp.getClientIp(req) 8 | 9 | module.exports = { getIP } 10 | -------------------------------------------------------------------------------- /app/middleware/utils/handleError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handles error by printing to console in development env and builds and sends an error response 3 | * @param {Object} res - response object 4 | * @param {Object} err - error object 5 | */ 6 | const handleError = (res = {}, err = {}) => { 7 | // Prints error in console 8 | if (process.env.NODE_ENV === 'development') { 9 | console.log(err) 10 | } 11 | // Sends error to user 12 | res.status(err.code).json({ 13 | errors: { 14 | msg: err.message 15 | } 16 | }) 17 | } 18 | 19 | module.exports = { handleError } 20 | -------------------------------------------------------------------------------- /app/middleware/utils/handleError.test.js: -------------------------------------------------------------------------------- 1 | const { handleError } = require('./handleError') 2 | 3 | const mockResponse = () => { 4 | const res = {} 5 | res.status = jest.fn().mockReturnValueOnce(res) 6 | res.json = jest.fn().mockReturnValueOnce(res) 7 | return res 8 | } 9 | 10 | const err = { 11 | message: 'error', 12 | code: 123 13 | } 14 | 15 | describe('handleError()', () => { 16 | it('should send the error object with the code and message provided and print the error code, message in development mode', async () => { 17 | process.env.NODE_ENV = 'development' 18 | 19 | const res = mockResponse() 20 | 21 | console.log = jest.fn() 22 | 23 | await handleError(res, err) 24 | 25 | expect(console.log).toHaveBeenCalledWith({ 26 | code: 123, 27 | message: 'error' 28 | }) 29 | 30 | expect(res.status).toHaveBeenCalledWith(123) 31 | 32 | expect(res.json).toHaveBeenCalledWith({ 33 | errors: { 34 | msg: 'error' 35 | } 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /app/middleware/utils/index.js: -------------------------------------------------------------------------------- 1 | const { buildErrObject } = require('./buildErrObject') 2 | const { buildSuccObject } = require('./buildSuccObject') 3 | const { getBrowserInfo } = require('./getBrowserInfo') 4 | const { getCountry } = require('./getCountry') 5 | const { getIP } = require('./getIP') 6 | const { handleError } = require('./handleError') 7 | const { isIDGood } = require('./isIDGood') 8 | const { itemNotFound } = require('./itemNotFound') 9 | const { removeExtensionFromFile } = require('./removeExtensionFromFile') 10 | const { validateResult } = require('./validateResult') 11 | 12 | module.exports = { 13 | buildErrObject, 14 | buildSuccObject, 15 | getBrowserInfo, 16 | getCountry, 17 | getIP, 18 | handleError, 19 | isIDGood, 20 | itemNotFound, 21 | removeExtensionFromFile, 22 | validateResult 23 | } 24 | -------------------------------------------------------------------------------- /app/middleware/utils/isIDGood.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const { buildErrObject } = require('./buildErrObject') 3 | 4 | /** 5 | * Checks if given ID is good for MongoDB 6 | * @param {string} id - id to check 7 | */ 8 | const isIDGood = async (id = '') => { 9 | return new Promise((resolve, reject) => { 10 | const goodID = mongoose.Types.ObjectId.isValid(id) 11 | return goodID ? resolve(id) : reject(buildErrObject(422, 'ID_MALFORMED')) 12 | }) 13 | } 14 | 15 | module.exports = { isIDGood } 16 | -------------------------------------------------------------------------------- /app/middleware/utils/itemNotFound.js: -------------------------------------------------------------------------------- 1 | const { buildErrObject } = require('./buildErrObject') 2 | 3 | /** 4 | * Item not found 5 | * @param {Object} err - error object 6 | * @param {Object} item - item result object 7 | * @param {string} message - message 8 | */ 9 | const itemNotFound = (err = {}, item = {}, message = 'NOT_FOUND') => { 10 | return new Promise((resolve, reject) => { 11 | if (err) { 12 | return reject(buildErrObject(422, err.message)) 13 | } 14 | if (!item) { 15 | return reject(buildErrObject(404, message)) 16 | } 17 | resolve() 18 | }) 19 | } 20 | 21 | module.exports = { itemNotFound } 22 | -------------------------------------------------------------------------------- /app/middleware/utils/removeExtensionFromFile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes extension from file 3 | * @param {string} file - filename 4 | */ 5 | const removeExtensionFromFile = (file) => { 6 | return file.split('.').slice(0, -1).join('.').toString() 7 | } 8 | 9 | module.exports = { removeExtensionFromFile } 10 | -------------------------------------------------------------------------------- /app/middleware/utils/validateResult.js: -------------------------------------------------------------------------------- 1 | const { validationResult } = require('express-validator') 2 | const { handleError } = require('./handleError') 3 | const { buildErrObject } = require('./buildErrObject') 4 | 5 | /** 6 | * Builds error for validation files 7 | * @param {Object} req - request object 8 | * @param {Object} res - response object 9 | * @param {Object} next - next object 10 | */ 11 | const validateResult = (req, res, next) => { 12 | try { 13 | validationResult(req).throw() 14 | if (req.body.email) { 15 | req.body.email = req.body.email.toLowerCase() 16 | } 17 | return next() 18 | } catch (err) { 19 | return handleError(res, buildErrObject(422, err.array())) 20 | } 21 | } 22 | 23 | module.exports = { validateResult } 24 | -------------------------------------------------------------------------------- /app/models/city.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const mongoosePaginate = require('mongoose-paginate-v2') 3 | 4 | const CitySchema = new mongoose.Schema( 5 | { 6 | name: { 7 | type: String, 8 | required: true 9 | } 10 | }, 11 | { 12 | versionKey: false, 13 | timestamps: true 14 | } 15 | ) 16 | CitySchema.plugin(mongoosePaginate) 17 | module.exports = mongoose.model('City', CitySchema) 18 | -------------------------------------------------------------------------------- /app/models/forgotPassword.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const validator = require('validator') 3 | 4 | const ForgotPasswordSchema = new mongoose.Schema( 5 | { 6 | email: { 7 | type: String, 8 | validate: { 9 | validator: validator.isEmail, 10 | message: 'EMAIL_IS_NOT_VALID' 11 | }, 12 | lowercase: true, 13 | required: true 14 | }, 15 | verification: { 16 | type: String 17 | }, 18 | used: { 19 | type: Boolean, 20 | default: false 21 | }, 22 | ipRequest: { 23 | type: String 24 | }, 25 | browserRequest: { 26 | type: String 27 | }, 28 | countryRequest: { 29 | type: String 30 | }, 31 | ipChanged: { 32 | type: String 33 | }, 34 | browserChanged: { 35 | type: String 36 | }, 37 | countryChanged: { 38 | type: String 39 | } 40 | }, 41 | { 42 | versionKey: false, 43 | timestamps: true 44 | } 45 | ) 46 | module.exports = mongoose.model('ForgotPassword', ForgotPasswordSchema) 47 | -------------------------------------------------------------------------------- /app/models/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const modelsPath = `${__dirname}/` 3 | const { removeExtensionFromFile } = require('../middleware/utils') 4 | 5 | module.exports = () => { 6 | /* 7 | * Load models dynamically 8 | */ 9 | 10 | // Loop models path and loads every file as a model except this file 11 | fs.readdirSync(modelsPath).filter((file) => { 12 | // Take filename and remove last part (extension) 13 | const modelFile = removeExtensionFromFile(file) 14 | // Prevents loading of this file 15 | return modelFile !== 'index' ? require(`./${modelFile}`) : '' 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const bcrypt = require('bcrypt') 3 | const validator = require('validator') 4 | const mongoosePaginate = require('mongoose-paginate-v2') 5 | 6 | const UserSchema = new mongoose.Schema( 7 | { 8 | name: { 9 | type: String, 10 | required: true 11 | }, 12 | email: { 13 | type: String, 14 | validate: { 15 | validator: validator.isEmail, 16 | message: 'EMAIL_IS_NOT_VALID' 17 | }, 18 | lowercase: true, 19 | unique: true, 20 | required: true 21 | }, 22 | password: { 23 | type: String, 24 | required: true, 25 | select: false 26 | }, 27 | role: { 28 | type: String, 29 | enum: ['user', 'admin'], 30 | default: 'user' 31 | }, 32 | verification: { 33 | type: String 34 | }, 35 | verified: { 36 | type: Boolean, 37 | default: false 38 | }, 39 | phone: { 40 | type: String 41 | }, 42 | city: { 43 | type: String 44 | }, 45 | country: { 46 | type: String 47 | }, 48 | urlTwitter: { 49 | type: String, 50 | validate: { 51 | validator(v) { 52 | return v === '' ? true : validator.isURL(v) 53 | }, 54 | message: 'NOT_A_VALID_URL' 55 | }, 56 | lowercase: true 57 | }, 58 | urlGitHub: { 59 | type: String, 60 | validate: { 61 | validator(v) { 62 | return v === '' ? true : validator.isURL(v) 63 | }, 64 | message: 'NOT_A_VALID_URL' 65 | }, 66 | lowercase: true 67 | }, 68 | loginAttempts: { 69 | type: Number, 70 | default: 0, 71 | select: false 72 | }, 73 | blockExpires: { 74 | type: Date, 75 | default: Date.now, 76 | select: false 77 | } 78 | }, 79 | { 80 | versionKey: false, 81 | timestamps: true 82 | } 83 | ) 84 | 85 | const hash = (user, salt, next) => { 86 | bcrypt.hash(user.password, salt, (error, newHash) => { 87 | if (error) { 88 | return next(error) 89 | } 90 | user.password = newHash 91 | return next() 92 | }) 93 | } 94 | 95 | const genSalt = (user, SALT_FACTOR, next) => { 96 | bcrypt.genSalt(SALT_FACTOR, (err, salt) => { 97 | if (err) { 98 | return next(err) 99 | } 100 | return hash(user, salt, next) 101 | }) 102 | } 103 | 104 | UserSchema.pre('save', function (next) { 105 | const that = this 106 | const SALT_FACTOR = 5 107 | if (!that.isModified('password')) { 108 | return next() 109 | } 110 | return genSalt(that, SALT_FACTOR, next) 111 | }) 112 | 113 | UserSchema.methods.comparePassword = function (passwordAttempt, cb) { 114 | bcrypt.compare(passwordAttempt, this.password, (err, isMatch) => 115 | err ? cb(err) : cb(null, isMatch) 116 | ) 117 | } 118 | UserSchema.plugin(mongoosePaginate) 119 | module.exports = mongoose.model('User', UserSchema) 120 | -------------------------------------------------------------------------------- /app/models/userAccess.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const validator = require('validator') 3 | 4 | const UserAccessSchema = new mongoose.Schema( 5 | { 6 | email: { 7 | type: String, 8 | validate: { 9 | validator: validator.isEmail, 10 | message: 'EMAIL_IS_NOT_VALID' 11 | }, 12 | lowercase: true, 13 | required: true 14 | }, 15 | ip: { 16 | type: String, 17 | required: true 18 | }, 19 | browser: { 20 | type: String, 21 | required: true 22 | }, 23 | country: { 24 | type: String, 25 | required: true 26 | } 27 | }, 28 | { 29 | versionKey: false, 30 | timestamps: true 31 | } 32 | ) 33 | module.exports = mongoose.model('UserAccess', UserAccessSchema) 34 | -------------------------------------------------------------------------------- /app/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | require('../../config/passport') 4 | const passport = require('passport') 5 | const requireAuth = passport.authenticate('jwt', { 6 | session: false 7 | }) 8 | const trimRequest = require('trim-request') 9 | 10 | const { 11 | register, 12 | verify, 13 | forgotPassword, 14 | resetPassword, 15 | getRefreshToken, 16 | login, 17 | roleAuthorization 18 | } = require('../controllers/auth') 19 | 20 | const { 21 | validateRegister, 22 | validateVerify, 23 | validateForgotPassword, 24 | validateResetPassword, 25 | validateLogin 26 | } = require('../controllers/auth/validators') 27 | 28 | /* 29 | * Auth routes 30 | */ 31 | 32 | /* 33 | * Register route 34 | */ 35 | router.post('/register', trimRequest.all, validateRegister, register) 36 | 37 | /* 38 | * Verify route 39 | */ 40 | router.post('/verify', trimRequest.all, validateVerify, verify) 41 | 42 | /* 43 | * Forgot password route 44 | */ 45 | router.post('/forgot', trimRequest.all, validateForgotPassword, forgotPassword) 46 | 47 | /* 48 | * Reset password route 49 | */ 50 | router.post('/reset', trimRequest.all, validateResetPassword, resetPassword) 51 | 52 | /* 53 | * Get new refresh token 54 | */ 55 | router.get( 56 | '/token', 57 | requireAuth, 58 | roleAuthorization(['user', 'admin']), 59 | trimRequest.all, 60 | getRefreshToken 61 | ) 62 | 63 | /* 64 | * Login route 65 | */ 66 | router.post('/login', trimRequest.all, validateLogin, login) 67 | 68 | module.exports = router 69 | -------------------------------------------------------------------------------- /app/routes/cities.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | require('../../config/passport') 4 | const passport = require('passport') 5 | const requireAuth = passport.authenticate('jwt', { 6 | session: false 7 | }) 8 | const trimRequest = require('trim-request') 9 | 10 | const { roleAuthorization } = require('../controllers/auth') 11 | 12 | const { 13 | getAllCities, 14 | getCities, 15 | createCity, 16 | getCity, 17 | updateCity, 18 | deleteCity 19 | } = require('../controllers/cities') 20 | 21 | const { 22 | validateCreateCity, 23 | validateGetCity, 24 | validateUpdateCity, 25 | validateDeleteCity 26 | } = require('../controllers/cities/validators') 27 | 28 | /* 29 | * Cities routes 30 | */ 31 | 32 | /* 33 | * Get all items route 34 | */ 35 | router.get('/all', getAllCities) 36 | 37 | /* 38 | * Get items route 39 | */ 40 | router.get( 41 | '/', 42 | requireAuth, 43 | roleAuthorization(['admin']), 44 | trimRequest.all, 45 | getCities 46 | ) 47 | 48 | /* 49 | * Create new item route 50 | */ 51 | router.post( 52 | '/', 53 | requireAuth, 54 | roleAuthorization(['admin']), 55 | trimRequest.all, 56 | validateCreateCity, 57 | createCity 58 | ) 59 | 60 | /* 61 | * Get item route 62 | */ 63 | router.get( 64 | '/:id', 65 | requireAuth, 66 | roleAuthorization(['admin']), 67 | trimRequest.all, 68 | validateGetCity, 69 | getCity 70 | ) 71 | 72 | /* 73 | * Update item route 74 | */ 75 | router.patch( 76 | '/:id', 77 | requireAuth, 78 | roleAuthorization(['admin']), 79 | trimRequest.all, 80 | validateUpdateCity, 81 | updateCity 82 | ) 83 | 84 | /* 85 | * Delete item route 86 | */ 87 | router.delete( 88 | '/:id', 89 | requireAuth, 90 | roleAuthorization(['admin']), 91 | trimRequest.all, 92 | validateDeleteCity, 93 | deleteCity 94 | ) 95 | 96 | module.exports = router 97 | -------------------------------------------------------------------------------- /app/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const fs = require('fs') 4 | const routesPath = `${__dirname}/` 5 | const { removeExtensionFromFile } = require('../middleware/utils') 6 | 7 | /* 8 | * Load routes statically and/or dynamically 9 | */ 10 | 11 | // Load Auth route 12 | router.use('/', require('./auth')) 13 | 14 | // Loop routes path and loads every file as a route except this file and Auth route 15 | fs.readdirSync(routesPath).filter((file) => { 16 | // Take filename and remove last part (extension) 17 | const routeFile = removeExtensionFromFile(file) 18 | // Prevents loading of this file and auth file 19 | return routeFile !== 'index' && routeFile !== 'auth' && file !== '.DS_Store' 20 | ? router.use(`/${routeFile}`, require(`./${routeFile}`)) 21 | : '' 22 | }) 23 | 24 | /* 25 | * Setup routes for index 26 | */ 27 | router.get('/', (req, res) => { 28 | res.render('index') 29 | }) 30 | 31 | /* 32 | * Handle 404 error 33 | */ 34 | router.use('*', (req, res) => { 35 | res.status(404).json({ 36 | errors: { 37 | msg: 'URL_NOT_FOUND' 38 | } 39 | }) 40 | }) 41 | 42 | module.exports = router 43 | -------------------------------------------------------------------------------- /app/routes/profile.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | require('../../config/passport') 4 | const passport = require('passport') 5 | const requireAuth = passport.authenticate('jwt', { 6 | session: false 7 | }) 8 | const trimRequest = require('trim-request') 9 | 10 | const { roleAuthorization } = require('../controllers/auth') 11 | 12 | const { 13 | getProfile, 14 | updateProfile, 15 | changePassword 16 | } = require('../controllers/profile') 17 | 18 | const { 19 | validateUpdateProfile, 20 | validateChangePassword 21 | } = require('../controllers/profile/validators') 22 | 23 | /* 24 | * Profile routes 25 | */ 26 | 27 | /* 28 | * Get profile route 29 | */ 30 | router.get( 31 | '/', 32 | requireAuth, 33 | roleAuthorization(['user', 'admin']), 34 | trimRequest.all, 35 | getProfile 36 | ) 37 | 38 | /* 39 | * Update profile route 40 | */ 41 | router.patch( 42 | '/', 43 | requireAuth, 44 | roleAuthorization(['user', 'admin']), 45 | trimRequest.all, 46 | validateUpdateProfile, 47 | updateProfile 48 | ) 49 | 50 | /* 51 | * Change password route 52 | */ 53 | router.post( 54 | '/changePassword', 55 | requireAuth, 56 | roleAuthorization(['user', 'admin']), 57 | trimRequest.all, 58 | validateChangePassword, 59 | changePassword 60 | ) 61 | 62 | module.exports = router 63 | -------------------------------------------------------------------------------- /app/routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | require('../../config/passport') 4 | const passport = require('passport') 5 | const requireAuth = passport.authenticate('jwt', { 6 | session: false 7 | }) 8 | const trimRequest = require('trim-request') 9 | 10 | const { roleAuthorization } = require('../controllers/auth') 11 | 12 | const { 13 | getUsers, 14 | createUser, 15 | getUser, 16 | updateUser, 17 | deleteUser 18 | } = require('../controllers/users') 19 | 20 | const { 21 | validateCreateUser, 22 | validateGetUser, 23 | validateUpdateUser, 24 | validateDeleteUser 25 | } = require('../controllers/users/validators') 26 | 27 | /* 28 | * Users routes 29 | */ 30 | 31 | /* 32 | * Get items route 33 | */ 34 | router.get( 35 | '/', 36 | requireAuth, 37 | roleAuthorization(['admin']), 38 | trimRequest.all, 39 | getUsers 40 | ) 41 | 42 | /* 43 | * Create new item route 44 | */ 45 | router.post( 46 | '/', 47 | requireAuth, 48 | roleAuthorization(['admin']), 49 | trimRequest.all, 50 | validateCreateUser, 51 | createUser 52 | ) 53 | 54 | /* 55 | * Get item route 56 | */ 57 | router.get( 58 | '/:id', 59 | requireAuth, 60 | roleAuthorization(['admin']), 61 | trimRequest.all, 62 | validateGetUser, 63 | getUser 64 | ) 65 | 66 | /* 67 | * Update item route 68 | */ 69 | router.patch( 70 | '/:id', 71 | requireAuth, 72 | roleAuthorization(['admin']), 73 | trimRequest.all, 74 | validateUpdateUser, 75 | updateUser 76 | ) 77 | 78 | /* 79 | * Delete item route 80 | */ 81 | router.delete( 82 | '/:id', 83 | requireAuth, 84 | roleAuthorization(['admin']), 85 | trimRequest.all, 86 | validateDeleteUser, 87 | deleteUser 88 | ) 89 | 90 | module.exports = router 91 | -------------------------------------------------------------------------------- /clean.js: -------------------------------------------------------------------------------- 1 | require('dotenv-safe').config() 2 | const initMongo = require('./config/mongo') 3 | const fs = require('fs') 4 | const modelsPath = `./app/models` 5 | const { removeExtensionFromFile } = require('./app/middleware/utils') 6 | 7 | initMongo() 8 | 9 | // Loop models path and loads every file as a model except index file 10 | const models = fs.readdirSync(modelsPath).filter((file) => { 11 | return removeExtensionFromFile(file) !== 'index' 12 | }) 13 | 14 | const deleteModelFromDB = (model) => { 15 | return new Promise((resolve, reject) => { 16 | model = require(`./app/models/${model}`) 17 | model.deleteMany({}, (err, row) => { 18 | if (err) { 19 | reject(err) 20 | } else { 21 | resolve(row) 22 | } 23 | }) 24 | }) 25 | } 26 | 27 | const clean = async () => { 28 | try { 29 | const promiseArray = models.map( 30 | async (model) => await deleteModelFromDB(model) 31 | ) 32 | await Promise.all(promiseArray) 33 | console.log('Cleanup complete!') 34 | process.exit(0) 35 | } catch (err) { 36 | console.log(err) 37 | process.exit(0) 38 | } 39 | } 40 | 41 | clean() 42 | -------------------------------------------------------------------------------- /config/mongo.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const DB_URL = process.env.MONGO_URI 3 | const loadModels = require('../app/models') 4 | 5 | module.exports = () => { 6 | const connect = () => { 7 | mongoose.Promise = global.Promise 8 | 9 | mongoose.connect( 10 | DB_URL, 11 | { 12 | keepAlive: true, 13 | useNewUrlParser: true, 14 | useUnifiedTopology: true 15 | }, 16 | (err) => { 17 | let dbStatus = '' 18 | if (err) { 19 | dbStatus = `* Error connecting to DB: ${err}\n****************************\n` 20 | } 21 | dbStatus = `* DB Connection: OK\n****************************\n` 22 | if (process.env.NODE_ENV !== 'test') { 23 | // Prints initialization 24 | console.log('****************************') 25 | console.log('* Starting Server') 26 | console.log(`* Port: ${process.env.PORT || 3000}`) 27 | console.log(`* NODE_ENV: ${process.env.NODE_ENV}`) 28 | console.log(`* Database: MongoDB`) 29 | console.log(dbStatus) 30 | } 31 | } 32 | ) 33 | mongoose.set('useCreateIndex', true) 34 | mongoose.set('useFindAndModify', false) 35 | } 36 | connect() 37 | 38 | mongoose.connection.on('error', console.log) 39 | mongoose.connection.on('disconnected', connect) 40 | 41 | loadModels() 42 | } 43 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport') 2 | const User = require('../app/models/user') 3 | const auth = require('../app/middleware/auth') 4 | const JwtStrategy = require('passport-jwt').Strategy 5 | 6 | /** 7 | * Extracts token from: header, body or query 8 | * @param {Object} req - request object 9 | * @returns {string} token - decrypted token 10 | */ 11 | const jwtExtractor = (req) => { 12 | let token = null 13 | if (req.headers.authorization) { 14 | token = req.headers.authorization.replace('Bearer ', '').trim() 15 | } else if (req.body.token) { 16 | token = req.body.token.trim() 17 | } else if (req.query.token) { 18 | token = req.query.token.trim() 19 | } 20 | if (token) { 21 | // Decrypts token 22 | token = auth.decrypt(token) 23 | } 24 | return token 25 | } 26 | 27 | /** 28 | * Options object for jwt middlware 29 | */ 30 | const jwtOptions = { 31 | jwtFromRequest: jwtExtractor, 32 | secretOrKey: process.env.JWT_SECRET 33 | } 34 | 35 | /** 36 | * Login with JWT middleware 37 | */ 38 | const jwtLogin = new JwtStrategy(jwtOptions, (payload, done) => { 39 | User.findById(payload.data._id, (err, user) => { 40 | if (err) { 41 | return done(err, false) 42 | } 43 | return !user ? done(null, false) : done(null, user) 44 | }) 45 | }) 46 | 47 | passport.use(jwtLogin) 48 | -------------------------------------------------------------------------------- /data/1.users/user.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker') 2 | const ObjectID = require('mongodb').ObjectID 3 | 4 | module.exports = [ 5 | { 6 | _id: new ObjectID('5aa1c2c35ef7a4e97b5e995a'), 7 | name: 'Super Administrator', 8 | email: 'admin@admin.com', 9 | password: '$2a$05$2KOSBnbb0r.0TmMrvefbluTOB735rF/KRZb4pmda4PdvU9iDvUB26', 10 | role: 'admin', 11 | verified: true, 12 | verification: '3d6e072c-0eaf-4239-bb5e-495e6486148f', 13 | city: 'Bucaramanga', 14 | country: 'Colombia', 15 | phone: '123123', 16 | urlTwitter: faker.internet.url(), 17 | urlGitHub: faker.internet.url(), 18 | createdAt: faker.date.past(), 19 | updatedAt: faker.date.recent() 20 | }, 21 | { 22 | _id: new ObjectID('5aa1c2c35ef7a4e97b5e995b'), 23 | name: 'Simple user', 24 | email: 'user@user.com', 25 | password: '$2a$05$2KOSBnbb0r.0TmMrvefbluTOB735rF/KRZb4pmda4PdvU9iDvUB26', 26 | role: 'user', 27 | verified: true, 28 | verification: '3d6e072c-0eaf-4239-bb5e-495e6486148d', 29 | city: 'Bucaramanga', 30 | country: 'Colombia', 31 | phone: '123123', 32 | urlTwitter: faker.internet.url(), 33 | urlGitHub: faker.internet.url(), 34 | createdAt: faker.date.past(), 35 | updatedAt: faker.date.recent() 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /data/2.cities/city.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker') 2 | 3 | const json = [ 4 | { 5 | name: 'San Francisco', 6 | createdAt: faker.date.past(), 7 | updatedAt: faker.date.recent() 8 | }, 9 | { 10 | name: 'New York', 11 | createdAt: faker.date.past(), 12 | updatedAt: faker.date.recent() 13 | }, 14 | { 15 | name: 'Chicago', 16 | createdAt: faker.date.past(), 17 | updatedAt: faker.date.recent() 18 | }, 19 | { 20 | name: 'Bogotá', 21 | createdAt: faker.date.past(), 22 | updatedAt: faker.date.recent() 23 | }, 24 | { 25 | name: 'Bucaramanga', 26 | createdAt: faker.date.past(), 27 | updatedAt: faker.date.recent() 28 | }, 29 | { 30 | name: 'Oakland', 31 | createdAt: faker.date.past(), 32 | updatedAt: faker.date.recent() 33 | }, 34 | { 35 | name: 'San Leandro', 36 | createdAt: faker.date.past(), 37 | updatedAt: faker.date.recent() 38 | }, 39 | { 40 | name: 'Medellín', 41 | createdAt: faker.date.past(), 42 | updatedAt: faker.date.recent() 43 | }, 44 | { 45 | name: 'Cali', 46 | createdAt: faker.date.past(), 47 | updatedAt: faker.date.recent() 48 | }, 49 | { 50 | name: 'Barranquilla', 51 | createdAt: faker.date.past(), 52 | updatedAt: faker.date.recent() 53 | } 54 | ] 55 | 56 | module.exports = json 57 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | testEnvironment: 'node', 4 | collectCoverage: true, 5 | collectCoverageFrom: [ 6 | '**/*.js', 7 | '!jest.config.js', 8 | '!**/data/**', 9 | '!**/node_modules/**', 10 | '!**/.history/**', 11 | '!**/test/**', 12 | '!**/coverage/**', 13 | '!**/tmp/**' 14 | ], 15 | coverageDirectory: 'coverage/unit', 16 | coverageReporters: ['json', 'text', 'lcov'], 17 | testPathIgnorePatterns: ['.history/'] 18 | } 19 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration": { 3 | "SUBJECT": "Verify your email at myProject", 4 | "MESSAGE": "

Hello %s.

Welcome! To verify your email, please click in this link:

%s/verify/%s

Thank you.

" 5 | }, 6 | "forgotPassword": { 7 | "SUBJECT": "Password recovery at myProject", 8 | "MESSAGE": "

To recover the password for user: %s

click the following link:

%s/reset/%s

If this was a mistake, you can ignore this message.

Thank you.

" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration": { 3 | "SUBJECT": "Verificar tu Email en myProject", 4 | "MESSAGE": "

Hola %s.

¡Bienvenido! Para verificar tu Email, por favor haz click en este enlace:

%s/verify/%s

Gracias.

" 5 | }, 6 | "forgotPassword": { 7 | "SUBJECT": "Recuperar contraseña en myProject", 8 | "MESSAGE": "

Para recuperar la contraseña para el usuario: %s

haz click en el siguiente enlace:

%s/reset/%s

Si esto fue un error, puedes ignorar este mensaje.

Gracias.

" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-express-mongodb-jwt-rest-api-skeleton", 3 | "version": "9.0.5", 4 | "description": "Node.js express.js MongoDB JWT REST API - This is a basic API REST skeleton written on JavaScript using async/await. Great for building a starter web API for your front-end (Android, iOS, Vue, react, angular, or anything that can consume an API)", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton.git" 9 | }, 10 | "scripts": { 11 | "start": "cross-env NODE_ENV=production pm2 start server.js", 12 | "mocha": "nyc mocha --timeout=5000 --exit", 13 | "test": "npm run coverage:clean && npm run test:unit && npm run test:e2e && npm run coverage", 14 | "test:unit": "cross-env NODE_ENV=test jest --coverage", 15 | "test:e2e": "cross-env NODE_ENV=test npm run fresh && npm run mocha", 16 | "dev": "cross-env NODE_ENV=development nodemon --inspect=9230 server.js", 17 | "fresh": "npm run clean && npm run seed", 18 | "clean": "node clean.js", 19 | "seed": "node seed.js", 20 | "prettier": "prettier --write --config .prettierrc.json \"**/*.js\"", 21 | "lint": "eslint --fix --config .eslintrc.json \"**/*.js\"", 22 | "remark": "remark . -o", 23 | "coverage": "npm run coverage:merge && npm run coverage:merge-report", 24 | "coverage:clean": "rm -rf .nyc_output && rm -rf coverage", 25 | "coverage:merge": "istanbul-merge --out coverage/merged/coverage-final.json ./coverage/unit/coverage-final.json ./coverage/e2e/coverage-final.json", 26 | "coverage:merge-report": "nyc report --reporter=lcov --reporter=text --reporter=json --temp-dir=./coverage/merged --report-dir=./coverage/merged" 27 | }, 28 | "nyc": { 29 | "reporter": [ 30 | "json", 31 | "text", 32 | "lcov" 33 | ], 34 | "report-dir": "coverage/e2e", 35 | "include": [ 36 | "**/*.js" 37 | ], 38 | "exclude": [ 39 | "**/*.test.js", 40 | "jest.config.js", 41 | "**/data/**", 42 | "**/node_modules/**", 43 | "**/.history/**", 44 | "**/test/**", 45 | "**/coverage/**", 46 | "**/tmp/**" 47 | ], 48 | "all": true 49 | }, 50 | "husky": { 51 | "hooks": { 52 | "pre-commit": "lint-staged && npm run remark", 53 | "pre-push": "npm run test" 54 | } 55 | }, 56 | "lint-staged": { 57 | "*.js": [ 58 | "prettier --write", 59 | "eslint --fix" 60 | ] 61 | }, 62 | "dependencies": { 63 | "babel-eslint": "^10.1.0", 64 | "bcrypt": "^5.0.1", 65 | "body-parser": "^1.20.0", 66 | "compression": "^1.7.4", 67 | "cors": "^2.8.5", 68 | "date-fns": "^2.28.0", 69 | "dotenv-safe": "^8.2.0", 70 | "ejs": "^3.1.7", 71 | "expeditious-engine-redis": "^0.1.2", 72 | "express": "^4.18.1", 73 | "express-expeditious": "^5.1.1", 74 | "express-validator": "^6.14.0", 75 | "helmet": "^4.6.0", 76 | "i18n": "^0.13.4", 77 | "jsonwebtoken": "^8.5.1", 78 | "mongoose": "^5.13.14", 79 | "mongoose-paginate-v2": "^1.6.3", 80 | "morgan": "^1.10.0", 81 | "nodemailer": "^6.7.4", 82 | "nodemailer-mailgun-transport": "^2.1.3", 83 | "passport": "^0.4.1", 84 | "passport-jwt": "^4.0.0", 85 | "request-ip": "^2.1.3", 86 | "trim-request": "^1.0.6", 87 | "uuid": "^8.3.2", 88 | "validator": "^13.7.0" 89 | }, 90 | "devDependencies": { 91 | "chai": "^4.3.6", 92 | "chai-http": "^4.3.0", 93 | "cross-env": "^7.0.3", 94 | "eslint": "^7.32.0", 95 | "eslint-config-formidable": "^4.0.0", 96 | "eslint-config-prettier": "^7.2.0", 97 | "eslint-plugin-prettier": "^3.4.1", 98 | "faker": "^5.5.3", 99 | "husky": "^4.3.8", 100 | "istanbul-merge": "^1.1.1", 101 | "jest": "^26.6.3", 102 | "lint-staged": "^10.5.4", 103 | "mocha": "^8.4.0", 104 | "mongo-seeding": "^3.7.1", 105 | "nodemon": "^2.0.16", 106 | "nyc": "^15.1.0", 107 | "prettier": "^2.6.2", 108 | "prettier-eslint": "^12.0.0", 109 | "remark-cli": "^9.0.0" 110 | }, 111 | "keywords": [ 112 | "javascript", 113 | "api", 114 | "node", 115 | "express", 116 | "mongo", 117 | "mongodb", 118 | "jwt", 119 | "postman", 120 | "i18n", 121 | "jwt-authentication", 122 | "token", 123 | "eslint", 124 | "starter", 125 | "web", 126 | "app", 127 | "mongoose", 128 | "rest", 129 | "skeleton", 130 | "async", 131 | "await", 132 | "mvp", 133 | "front-end", 134 | "testing", 135 | "prettier", 136 | "mocha", 137 | "chai", 138 | "redis", 139 | "JSDoc" 140 | ] 141 | } 142 | -------------------------------------------------------------------------------- /posman-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "b82598d1-5f65-4310-bf0e-ba31414862b1", 4 | "name": "api node mongo jwt", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "auth", 10 | "item": [ 11 | { 12 | "name": "/register", 13 | "request": { 14 | "auth": { 15 | "type": "noauth" 16 | }, 17 | "method": "POST", 18 | "header": [ 19 | { 20 | "key": "Content-Type", 21 | "value": "application/x-www-form-urlencoded" 22 | }, 23 | { 24 | "key": "Accept-Language", 25 | "value": "en", 26 | "type": "text" 27 | } 28 | ], 29 | "body": { 30 | "mode": "urlencoded", 31 | "urlencoded": [ 32 | { 33 | "key": "name", 34 | "value": "My Name", 35 | "type": "text" 36 | }, 37 | { 38 | "key": "email", 39 | "value": "my@email.com", 40 | "type": "text" 41 | }, 42 | { 43 | "key": "password", 44 | "value": "12345", 45 | "type": "text" 46 | } 47 | ] 48 | }, 49 | "url": { 50 | "raw": "{{server}}/register", 51 | "host": [ 52 | "{{server}}" 53 | ], 54 | "path": [ 55 | "register" 56 | ] 57 | } 58 | }, 59 | "response": [] 60 | }, 61 | { 62 | "name": "/verify", 63 | "request": { 64 | "auth": { 65 | "type": "noauth" 66 | }, 67 | "method": "POST", 68 | "header": [ 69 | { 70 | "key": "Content-Type", 71 | "value": "application/x-www-form-urlencoded" 72 | } 73 | ], 74 | "body": { 75 | "mode": "urlencoded", 76 | "urlencoded": [ 77 | { 78 | "key": "id", 79 | "value": "b98b1dea-b3f4-4b72-bcdf-9d36607e2603", 80 | "type": "text" 81 | } 82 | ] 83 | }, 84 | "url": { 85 | "raw": "{{server}}/verify", 86 | "host": [ 87 | "{{server}}" 88 | ], 89 | "path": [ 90 | "verify" 91 | ] 92 | } 93 | }, 94 | "response": [] 95 | }, 96 | { 97 | "name": "/forgot", 98 | "request": { 99 | "auth": { 100 | "type": "noauth" 101 | }, 102 | "method": "POST", 103 | "header": [ 104 | { 105 | "key": "Content-Type", 106 | "value": "application/x-www-form-urlencoded" 107 | }, 108 | { 109 | "key": "Accept-Language", 110 | "value": "en", 111 | "type": "text" 112 | } 113 | ], 114 | "body": { 115 | "mode": "urlencoded", 116 | "urlencoded": [ 117 | { 118 | "key": "email", 119 | "value": "admin@admin.com", 120 | "type": "text" 121 | } 122 | ] 123 | }, 124 | "url": { 125 | "raw": "{{server}}/forgot", 126 | "host": [ 127 | "{{server}}" 128 | ], 129 | "path": [ 130 | "forgot" 131 | ] 132 | } 133 | }, 134 | "response": [] 135 | }, 136 | { 137 | "name": "/reset", 138 | "request": { 139 | "auth": { 140 | "type": "noauth" 141 | }, 142 | "method": "POST", 143 | "header": [ 144 | { 145 | "key": "Content-Type", 146 | "value": "application/x-www-form-urlencoded" 147 | } 148 | ], 149 | "body": { 150 | "mode": "urlencoded", 151 | "urlencoded": [ 152 | { 153 | "key": "id", 154 | "value": "f5b08991-02fc-4f3d-9a32-6d703bef5c57", 155 | "type": "text" 156 | }, 157 | { 158 | "key": "password", 159 | "value": "12345", 160 | "type": "text" 161 | } 162 | ] 163 | }, 164 | "url": { 165 | "raw": "{{server}}/reset", 166 | "host": [ 167 | "{{server}}" 168 | ], 169 | "path": [ 170 | "reset" 171 | ] 172 | } 173 | }, 174 | "response": [] 175 | }, 176 | { 177 | "name": "/login", 178 | "event": [ 179 | { 180 | "listen": "test", 181 | "script": { 182 | "id": "76965795-61c0-4767-8b82-8dc56635b784", 183 | "exec": [ 184 | "//- auto populates token to environment variables", 185 | "pm.test(\"Token returned from login, added to environment.\", function () {", 186 | " pm.expect(pm.response.text()).to.include(\"token\");", 187 | " const jsonData = pm.response.json();", 188 | " pm.environment.set('authToken', jsonData.token);", 189 | "});" 190 | ], 191 | "type": "text/javascript" 192 | } 193 | } 194 | ], 195 | "request": { 196 | "auth": { 197 | "type": "noauth" 198 | }, 199 | "method": "POST", 200 | "header": [ 201 | { 202 | "key": "Content-Type", 203 | "value": "application/x-www-form-urlencoded" 204 | } 205 | ], 206 | "body": { 207 | "mode": "urlencoded", 208 | "urlencoded": [ 209 | { 210 | "key": "email", 211 | "value": "admin@admin.com", 212 | "type": "text" 213 | }, 214 | { 215 | "key": "password", 216 | "value": "12345", 217 | "type": "text" 218 | } 219 | ] 220 | }, 221 | "url": { 222 | "raw": "{{server}}/login", 223 | "host": [ 224 | "{{server}}" 225 | ], 226 | "path": [ 227 | "login" 228 | ] 229 | } 230 | }, 231 | "response": [] 232 | }, 233 | { 234 | "name": "/token", 235 | "request": { 236 | "method": "GET", 237 | "header": [ 238 | { 239 | "key": "Authorization", 240 | "value": "Bearer {{authToken}}" 241 | } 242 | ], 243 | "url": { 244 | "raw": "{{server}}/token", 245 | "host": [ 246 | "{{server}}" 247 | ], 248 | "path": [ 249 | "token" 250 | ] 251 | } 252 | }, 253 | "response": [] 254 | } 255 | ] 256 | }, 257 | { 258 | "name": "users", 259 | "item": [ 260 | { 261 | "name": "/users", 262 | "request": { 263 | "method": "GET", 264 | "header": [ 265 | { 266 | "key": "Authorization", 267 | "value": "Bearer {{authToken}}" 268 | } 269 | ], 270 | "url": { 271 | "raw": "{{server}}/users?filter=ad&fields=name,email&page=1&limit=10&sort=name&order=-1", 272 | "host": [ 273 | "{{server}}" 274 | ], 275 | "path": [ 276 | "users" 277 | ], 278 | "query": [ 279 | { 280 | "key": "filter", 281 | "value": "ad" 282 | }, 283 | { 284 | "key": "fields", 285 | "value": "name,email" 286 | }, 287 | { 288 | "key": "page", 289 | "value": "1" 290 | }, 291 | { 292 | "key": "limit", 293 | "value": "10" 294 | }, 295 | { 296 | "key": "sort", 297 | "value": "name" 298 | }, 299 | { 300 | "key": "order", 301 | "value": "-1" 302 | } 303 | ] 304 | } 305 | }, 306 | "response": [] 307 | }, 308 | { 309 | "name": "/users", 310 | "request": { 311 | "method": "POST", 312 | "header": [ 313 | { 314 | "key": "Authorization", 315 | "value": "Bearer {{authToken}}" 316 | }, 317 | { 318 | "key": "Content-Type", 319 | "value": "application/x-www-form-urlencoded" 320 | } 321 | ], 322 | "body": { 323 | "mode": "urlencoded", 324 | "urlencoded": [ 325 | { 326 | "key": "name", 327 | "value": "New User", 328 | "type": "text" 329 | }, 330 | { 331 | "key": "email", 332 | "value": "myemail@email.com", 333 | "type": "text" 334 | }, 335 | { 336 | "key": "password", 337 | "value": "12345", 338 | "type": "text" 339 | }, 340 | { 341 | "key": "role", 342 | "value": "admin", 343 | "type": "text" 344 | }, 345 | { 346 | "key": "phone", 347 | "value": "123123", 348 | "type": "text" 349 | }, 350 | { 351 | "key": "city", 352 | "value": "Bucaramamga", 353 | "type": "text" 354 | }, 355 | { 356 | "key": "country", 357 | "value": "Colombia", 358 | "type": "text" 359 | } 360 | ] 361 | }, 362 | "url": { 363 | "raw": "{{server}}/users", 364 | "host": [ 365 | "{{server}}" 366 | ], 367 | "path": [ 368 | "users" 369 | ] 370 | } 371 | }, 372 | "response": [] 373 | }, 374 | { 375 | "name": "/users/:id", 376 | "request": { 377 | "method": "DELETE", 378 | "header": [ 379 | { 380 | "key": "Authorization", 381 | "value": "Bearer {{authToken}}" 382 | } 383 | ], 384 | "body": { 385 | "mode": "formdata", 386 | "formdata": [] 387 | }, 388 | "url": { 389 | "raw": "{{server}}/users/5aab2443ef417d2d19e6c8f2", 390 | "host": [ 391 | "{{server}}" 392 | ], 393 | "path": [ 394 | "users", 395 | "5aab2443ef417d2d19e6c8f2" 396 | ] 397 | } 398 | }, 399 | "response": [] 400 | }, 401 | { 402 | "name": "/users/:id", 403 | "request": { 404 | "method": "GET", 405 | "header": [ 406 | { 407 | "key": "Authorization", 408 | "value": "Bearer {{authToken}}" 409 | } 410 | ], 411 | "url": { 412 | "raw": "{{server}}/users/5aa1c2c35ef7a4e97b5e995a", 413 | "host": [ 414 | "{{server}}" 415 | ], 416 | "path": [ 417 | "users", 418 | "5aa1c2c35ef7a4e97b5e995a" 419 | ] 420 | } 421 | }, 422 | "response": [] 423 | }, 424 | { 425 | "name": "/users/:id", 426 | "request": { 427 | "method": "PATCH", 428 | "header": [ 429 | { 430 | "key": "Authorization", 431 | "value": "Bearer {{authToken}}" 432 | }, 433 | { 434 | "key": "Content-Type", 435 | "value": "application/x-www-form-urlencoded" 436 | } 437 | ], 438 | "body": { 439 | "mode": "urlencoded", 440 | "urlencoded": [ 441 | { 442 | "key": "name", 443 | "value": "New Name", 444 | "type": "text" 445 | }, 446 | { 447 | "key": "email", 448 | "value": "new@email.com", 449 | "type": "text" 450 | }, 451 | { 452 | "key": "role", 453 | "value": "admin", 454 | "type": "text" 455 | }, 456 | { 457 | "key": "phone", 458 | "value": "12345", 459 | "type": "text" 460 | }, 461 | { 462 | "key": "city", 463 | "value": "Cali", 464 | "type": "text" 465 | }, 466 | { 467 | "key": "country", 468 | "value": "Colombia", 469 | "type": "text" 470 | } 471 | ] 472 | }, 473 | "url": { 474 | "raw": "http://localhost:3000/users/5aa1c2c35ef7a4e97b5e995a?", 475 | "protocol": "http", 476 | "host": [ 477 | "localhost" 478 | ], 479 | "port": "3000", 480 | "path": [ 481 | "users", 482 | "5aa1c2c35ef7a4e97b5e995a" 483 | ], 484 | "query": [ 485 | { 486 | "key": "sort", 487 | "value": "id ASC", 488 | "disabled": true 489 | }, 490 | { 491 | "key": "skip", 492 | "value": "3", 493 | "disabled": true 494 | }, 495 | { 496 | "key": "where", 497 | "value": "{ 'email' : { startsWith : 'f' } }", 498 | "disabled": true 499 | } 500 | ] 501 | } 502 | }, 503 | "response": [] 504 | } 505 | ] 506 | }, 507 | { 508 | "name": "profile", 509 | "item": [ 510 | { 511 | "name": "/profile", 512 | "request": { 513 | "method": "GET", 514 | "header": [ 515 | { 516 | "key": "Authorization", 517 | "value": "Bearer {{authToken}}" 518 | } 519 | ], 520 | "url": { 521 | "raw": "{{server}}/profile", 522 | "host": [ 523 | "{{server}}" 524 | ], 525 | "path": [ 526 | "profile" 527 | ] 528 | } 529 | }, 530 | "response": [] 531 | }, 532 | { 533 | "name": "/profile", 534 | "request": { 535 | "method": "PATCH", 536 | "header": [ 537 | { 538 | "key": "Authorization", 539 | "value": "Bearer {{authToken}}" 540 | }, 541 | { 542 | "key": "Content-Type", 543 | "value": "application/x-www-form-urlencoded" 544 | } 545 | ], 546 | "body": { 547 | "mode": "urlencoded", 548 | "urlencoded": [ 549 | { 550 | "key": "name", 551 | "value": "My Name", 552 | "type": "text" 553 | }, 554 | { 555 | "key": "urlTwitter", 556 | "value": "https://hello.com", 557 | "type": "text" 558 | }, 559 | { 560 | "key": "urlGitHub", 561 | "value": "https://hello.io", 562 | "type": "text" 563 | }, 564 | { 565 | "key": "phone", 566 | "value": "444444", 567 | "type": "text" 568 | }, 569 | { 570 | "key": "city", 571 | "value": "Boston2", 572 | "type": "text" 573 | }, 574 | { 575 | "key": "country", 576 | "value": "Colombia2", 577 | "type": "text" 578 | } 579 | ] 580 | }, 581 | "url": { 582 | "raw": "{{server}}/profile", 583 | "host": [ 584 | "{{server}}" 585 | ], 586 | "path": [ 587 | "profile" 588 | ] 589 | } 590 | }, 591 | "response": [] 592 | }, 593 | { 594 | "name": "/profile/changePassword", 595 | "request": { 596 | "method": "POST", 597 | "header": [ 598 | { 599 | "key": "Authorization", 600 | "value": "Bearer {{authToken}}" 601 | }, 602 | { 603 | "key": "Content-Type", 604 | "value": "application/x-www-form-urlencoded" 605 | } 606 | ], 607 | "body": { 608 | "mode": "urlencoded", 609 | "urlencoded": [ 610 | { 611 | "key": "oldPassword", 612 | "value": "12345", 613 | "type": "text" 614 | }, 615 | { 616 | "key": "newPassword", 617 | "value": "12345", 618 | "type": "text" 619 | } 620 | ] 621 | }, 622 | "url": { 623 | "raw": "{{server}}/profile/changePassword", 624 | "host": [ 625 | "{{server}}" 626 | ], 627 | "path": [ 628 | "profile", 629 | "changePassword" 630 | ] 631 | } 632 | }, 633 | "response": [] 634 | } 635 | ] 636 | }, 637 | { 638 | "name": "cities", 639 | "item": [ 640 | { 641 | "name": "/cities/all", 642 | "request": { 643 | "method": "GET", 644 | "header": [ 645 | { 646 | "key": "Authorization", 647 | "value": "Bearer {{authToken}}" 648 | } 649 | ], 650 | "url": { 651 | "raw": "{{server}}/cities/all", 652 | "host": [ 653 | "{{server}}" 654 | ], 655 | "path": [ 656 | "cities", 657 | "all" 658 | ] 659 | } 660 | }, 661 | "response": [] 662 | }, 663 | { 664 | "name": "/cities", 665 | "request": { 666 | "method": "GET", 667 | "header": [ 668 | { 669 | "key": "Authorization", 670 | "value": "Bearer {{authToken}}" 671 | } 672 | ], 673 | "url": { 674 | "raw": "{{server}}/cities?filter=Bucaramanga&fields=name&page=1&limit=5&sort=name&order=1", 675 | "host": [ 676 | "{{server}}" 677 | ], 678 | "path": [ 679 | "cities" 680 | ], 681 | "query": [ 682 | { 683 | "key": "filter", 684 | "value": "Bucaramanga" 685 | }, 686 | { 687 | "key": "fields", 688 | "value": "name" 689 | }, 690 | { 691 | "key": "page", 692 | "value": "1" 693 | }, 694 | { 695 | "key": "limit", 696 | "value": "5" 697 | }, 698 | { 699 | "key": "sort", 700 | "value": "name" 701 | }, 702 | { 703 | "key": "order", 704 | "value": "1" 705 | } 706 | ] 707 | } 708 | }, 709 | "response": [] 710 | }, 711 | { 712 | "name": "/cities", 713 | "request": { 714 | "method": "POST", 715 | "header": [ 716 | { 717 | "key": "Authorization", 718 | "value": "Bearer {{authToken}}" 719 | }, 720 | { 721 | "key": "Content-Type", 722 | "value": "application/x-www-form-urlencoded" 723 | } 724 | ], 725 | "body": { 726 | "mode": "urlencoded", 727 | "urlencoded": [ 728 | { 729 | "key": "name", 730 | "value": "Miami", 731 | "type": "text" 732 | } 733 | ] 734 | }, 735 | "url": { 736 | "raw": "{{server}}/cities", 737 | "host": [ 738 | "{{server}}" 739 | ], 740 | "path": [ 741 | "cities" 742 | ] 743 | } 744 | }, 745 | "response": [] 746 | }, 747 | { 748 | "name": "/cities/:id", 749 | "request": { 750 | "method": "GET", 751 | "header": [ 752 | { 753 | "key": "Authorization", 754 | "value": "Bearer {{authToken}}" 755 | }, 756 | { 757 | "key": "Accept-Language", 758 | "value": "es", 759 | "type": "text" 760 | } 761 | ], 762 | "url": { 763 | "raw": "{{server}}/cities/5bd08db979bbc504c14ebfdd", 764 | "host": [ 765 | "{{server}}" 766 | ], 767 | "path": [ 768 | "cities", 769 | "5bd08db979bbc504c14ebfdd" 770 | ] 771 | } 772 | }, 773 | "response": [] 774 | }, 775 | { 776 | "name": "/cities/:id", 777 | "request": { 778 | "method": "PATCH", 779 | "header": [ 780 | { 781 | "key": "Authorization", 782 | "value": "Bearer {{authToken}}" 783 | }, 784 | { 785 | "key": "Content-Type", 786 | "value": "application/x-www-form-urlencoded" 787 | } 788 | ], 789 | "body": { 790 | "mode": "urlencoded", 791 | "urlencoded": [ 792 | { 793 | "key": "name", 794 | "value": "Seatle2", 795 | "type": "text" 796 | } 797 | ] 798 | }, 799 | "url": { 800 | "raw": "{{server}}/cities/5bd08db979bbc504c14ebfdd?", 801 | "host": [ 802 | "{{server}}" 803 | ], 804 | "path": [ 805 | "cities", 806 | "5bd08db979bbc504c14ebfdd" 807 | ], 808 | "query": [ 809 | { 810 | "key": "sort", 811 | "value": "id ASC", 812 | "disabled": true 813 | }, 814 | { 815 | "key": "skip", 816 | "value": "3", 817 | "disabled": true 818 | }, 819 | { 820 | "key": "where", 821 | "value": "{ 'email' : { startsWith : 'f' } }", 822 | "disabled": true 823 | } 824 | ] 825 | } 826 | }, 827 | "response": [] 828 | }, 829 | { 830 | "name": "/cities/:id", 831 | "request": { 832 | "method": "DELETE", 833 | "header": [ 834 | { 835 | "key": "Authorization", 836 | "value": "Bearer {{authToken}}" 837 | } 838 | ], 839 | "body": { 840 | "mode": "formdata", 841 | "formdata": [] 842 | }, 843 | "url": { 844 | "raw": "{{server}}/cities/5b38171b1843d58427c5d6c4", 845 | "host": [ 846 | "{{server}}" 847 | ], 848 | "path": [ 849 | "cities", 850 | "5b38171b1843d58427c5d6c4" 851 | ] 852 | } 853 | }, 854 | "response": [] 855 | } 856 | ] 857 | } 858 | ] 859 | } -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davellanedam/node-express-mongodb-jwt-rest-api-skeleton/50b15eb84fb6d94a6a6f6940b43e9e614770fa53/public/.gitkeep -------------------------------------------------------------------------------- /seed.js: -------------------------------------------------------------------------------- 1 | require('dotenv-safe').config() 2 | const { Seeder } = require('mongo-seeding') 3 | const path = require('path') 4 | const config = { 5 | database: process.env.MONGO_URI, 6 | inputPath: path.resolve(__dirname, './data'), 7 | dropDatabase: false 8 | } 9 | const seeder = new Seeder(config) 10 | const collections = seeder.readCollectionsFromPath(path.resolve('./data')) 11 | 12 | const main = async () => { 13 | try { 14 | await seeder.import(collections) 15 | console.log('Seed complete!') 16 | process.exit(0) 17 | } catch (err) { 18 | console.log(err) 19 | process.exit(0) 20 | } 21 | } 22 | 23 | main() 24 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv-safe').config() 2 | const express = require('express') 3 | const bodyParser = require('body-parser') 4 | const morgan = require('morgan') 5 | const compression = require('compression') 6 | const helmet = require('helmet') 7 | const cors = require('cors') 8 | const passport = require('passport') 9 | const app = express() 10 | const i18n = require('i18n') 11 | const initMongo = require('./config/mongo') 12 | const path = require('path') 13 | 14 | // Setup express server port from ENV, default: 3000 15 | app.set('port', process.env.PORT || 3000) 16 | 17 | // Enable only in development HTTP request logger middleware 18 | if (process.env.NODE_ENV === 'development') { 19 | app.use(morgan('dev')) 20 | } 21 | 22 | // Redis cache enabled by env variable 23 | if (process.env.USE_REDIS === 'true') { 24 | const getExpeditiousCache = require('express-expeditious') 25 | const cache = getExpeditiousCache({ 26 | namespace: 'expresscache', 27 | defaultTtl: '1 minute', 28 | engine: require('expeditious-engine-redis')({ 29 | redis: { 30 | host: process.env.REDIS_HOST, 31 | port: process.env.REDIS_PORT 32 | } 33 | }) 34 | }) 35 | app.use(cache) 36 | } 37 | 38 | // for parsing json 39 | app.use( 40 | bodyParser.json({ 41 | limit: '20mb' 42 | }) 43 | ) 44 | // for parsing application/x-www-form-urlencoded 45 | app.use( 46 | bodyParser.urlencoded({ 47 | limit: '20mb', 48 | extended: true 49 | }) 50 | ) 51 | 52 | // i18n 53 | i18n.configure({ 54 | locales: ['en', 'es'], 55 | directory: `${__dirname}/locales`, 56 | defaultLocale: 'en', 57 | objectNotation: true 58 | }) 59 | app.use(i18n.init) 60 | 61 | // Init all other stuff 62 | app.use(cors()) 63 | app.use(passport.initialize()) 64 | app.use(compression()) 65 | app.use(helmet()) 66 | app.use(express.static('public')) 67 | app.set('views', path.join(__dirname, 'views')) 68 | app.engine('html', require('ejs').renderFile) 69 | app.set('view engine', 'html') 70 | app.use(require('./app/routes')) 71 | app.listen(app.get('port')) 72 | 73 | // Init MongoDB 74 | initMongo() 75 | 76 | module.exports = app // for testing 77 | -------------------------------------------------------------------------------- /test/auth.js: -------------------------------------------------------------------------------- 1 | /* eslint handle-callback-err: "off"*/ 2 | 3 | process.env.NODE_ENV = 'test' 4 | 5 | const User = require('../app/models/user') 6 | const faker = require('faker') 7 | const chai = require('chai') 8 | const chaiHttp = require('chai-http') 9 | const server = require('../server') 10 | // eslint-disable-next-line no-unused-vars 11 | const should = chai.should() 12 | const loginDetails = { 13 | email: 'admin@admin.com', 14 | password: '12345' 15 | } 16 | let token = '' 17 | const createdID = [] 18 | let verification = '' 19 | let verificationForgot = '' 20 | const email = faker.internet.email() 21 | const failedLoginAttempts = 5 22 | const badUser = { 23 | name: 'Bad user', 24 | email: 'bad@user.com', 25 | password: '54321' 26 | } 27 | const badLoginDetails = { 28 | email: 'bad@user.com', 29 | password: '12345' 30 | } 31 | 32 | chai.use(chaiHttp) 33 | 34 | describe('*********** AUTH ***********', () => { 35 | describe('/GET /', () => { 36 | it('it should GET home API url', (done) => { 37 | chai 38 | .request(server) 39 | .get('/') 40 | .end((err, res) => { 41 | res.should.have.status(200) 42 | done() 43 | }) 44 | }) 45 | }) 46 | 47 | describe('/GET /404url', () => { 48 | it('it should GET 404 url', (done) => { 49 | chai 50 | .request(server) 51 | .get('/404url') 52 | .end((err, res) => { 53 | res.should.have.status(404) 54 | res.body.should.be.an('object') 55 | done() 56 | }) 57 | }) 58 | }) 59 | 60 | describe('/POST login', () => { 61 | it('it should GET token', (done) => { 62 | chai 63 | .request(server) 64 | .post('/login') 65 | .send(loginDetails) 66 | .end((err, res) => { 67 | res.should.have.status(200) 68 | res.body.should.be.an('object') 69 | res.body.should.have.property('token') 70 | token = res.body.token 71 | done() 72 | }) 73 | }) 74 | }) 75 | 76 | describe('/POST register', () => { 77 | it('it should POST register', (done) => { 78 | const user = { 79 | name: faker.random.words(), 80 | email, 81 | password: faker.random.words() 82 | } 83 | chai 84 | .request(server) 85 | .post('/register') 86 | .send(user) 87 | .end((err, res) => { 88 | res.should.have.status(201) 89 | res.body.should.be.an('object') 90 | res.body.should.include.keys('token', 'user') 91 | createdID.push(res.body.user._id) 92 | verification = res.body.user.verification 93 | done() 94 | }) 95 | }) 96 | it('it should NOT POST a register if email already exists', (done) => { 97 | const user = { 98 | name: faker.random.words(), 99 | email, 100 | password: faker.random.words() 101 | } 102 | chai 103 | .request(server) 104 | .post('/register') 105 | .send(user) 106 | .end((err, res) => { 107 | res.should.have.status(422) 108 | res.body.should.be.a('object') 109 | res.body.should.have.property('errors') 110 | done() 111 | }) 112 | }) 113 | }) 114 | 115 | describe('/POST verify', () => { 116 | it('it should POST verify', (done) => { 117 | chai 118 | .request(server) 119 | .post('/verify') 120 | .send({ 121 | id: verification 122 | }) 123 | .end((err, res) => { 124 | res.should.have.status(200) 125 | res.body.should.be.an('object') 126 | res.body.should.include.keys('email', 'verified') 127 | res.body.verified.should.equal(true) 128 | done() 129 | }) 130 | }) 131 | }) 132 | 133 | describe('/POST forgot', () => { 134 | it('it should POST forgot', (done) => { 135 | chai 136 | .request(server) 137 | .post('/forgot') 138 | .send({ 139 | email 140 | }) 141 | .end((err, res) => { 142 | res.should.have.status(200) 143 | res.body.should.be.an('object') 144 | res.body.should.include.keys('msg', 'verification') 145 | verificationForgot = res.body.verification 146 | done() 147 | }) 148 | }) 149 | }) 150 | 151 | describe('/POST reset', () => { 152 | it('it should POST reset', (done) => { 153 | chai 154 | .request(server) 155 | .post('/reset') 156 | .send({ 157 | id: verificationForgot, 158 | password: '12345' 159 | }) 160 | .end((err, res) => { 161 | res.should.have.status(200) 162 | res.body.should.be.a('object') 163 | res.body.should.have.property('msg').eql('PASSWORD_CHANGED') 164 | done() 165 | }) 166 | }) 167 | }) 168 | 169 | describe('/GET token', () => { 170 | it('it should NOT be able to consume the route since no token was sent', (done) => { 171 | chai 172 | .request(server) 173 | .get('/token') 174 | .end((err, res) => { 175 | res.should.have.status(401) 176 | done() 177 | }) 178 | }) 179 | it('it should GET a fresh token', (done) => { 180 | chai 181 | .request(server) 182 | .get('/token') 183 | .set('Authorization', `Bearer ${token}`) 184 | .end((err, res) => { 185 | res.should.have.status(200) 186 | res.body.should.be.an('object') 187 | res.body.should.have.property('token') 188 | done() 189 | }) 190 | }) 191 | }) 192 | 193 | describe('/POST register', () => { 194 | it('it should POST register', (done) => { 195 | chai 196 | .request(server) 197 | .post('/register') 198 | .send(badUser) 199 | .end((err, res) => { 200 | res.should.have.status(201) 201 | res.body.should.be.an('object') 202 | res.body.should.include.keys('token', 'user') 203 | createdID.push(res.body.user._id) 204 | done() 205 | }) 206 | }) 207 | }) 208 | 209 | describe('/POST login', () => { 210 | for (let x = 1; x < failedLoginAttempts + 1; x++) { 211 | it(`it should NOT POST login after password fail #${x}`, (done) => { 212 | chai 213 | .request(server) 214 | .post('/login') 215 | .send(badLoginDetails) 216 | .end((err, res) => { 217 | res.should.have.status(409) 218 | res.body.should.be.a('object') 219 | res.body.should.have.property('errors').that.has.property('msg') 220 | res.body.errors.should.have.property('msg').eql('WRONG_PASSWORD') 221 | done() 222 | }) 223 | }) 224 | } 225 | 226 | it('it should NOT POST login after password fail #6 and be blocked', (done) => { 227 | chai 228 | .request(server) 229 | .post('/login') 230 | .send(badLoginDetails) 231 | .end((err, res) => { 232 | res.should.have.status(409) 233 | res.body.should.be.a('object') 234 | res.body.should.have.property('errors').that.has.property('msg') 235 | res.body.errors.should.have.property('msg').eql('BLOCKED_USER') 236 | done() 237 | }) 238 | }) 239 | 240 | it('it should NOT POST login after being blocked sending post with correct password', (done) => { 241 | chai 242 | .request(server) 243 | .post('/login') 244 | .send({ 245 | email: badUser.email, 246 | password: badUser.password 247 | }) 248 | .end((err, res) => { 249 | res.should.have.status(409) 250 | res.body.should.be.a('object') 251 | res.body.should.have.property('errors').that.has.property('msg') 252 | res.body.errors.should.have.property('msg').eql('BLOCKED_USER') 253 | done() 254 | }) 255 | }) 256 | }) 257 | after(() => { 258 | createdID.forEach((id) => { 259 | User.findByIdAndRemove(id, (err) => { 260 | if (err) { 261 | console.log(err) 262 | } 263 | }) 264 | }) 265 | }) 266 | }) 267 | -------------------------------------------------------------------------------- /test/cities.js: -------------------------------------------------------------------------------- 1 | /* eslint handle-callback-err: "off"*/ 2 | 3 | process.env.NODE_ENV = 'test' 4 | 5 | const City = require('../app/models/city') 6 | const faker = require('faker') 7 | const chai = require('chai') 8 | const chaiHttp = require('chai-http') 9 | const server = require('../server') 10 | // eslint-disable-next-line no-unused-vars 11 | const should = chai.should() 12 | const loginDetails = { 13 | email: 'admin@admin.com', 14 | password: '12345' 15 | } 16 | let token = '' 17 | const createdID = [] 18 | const name = faker.random.words() 19 | const newName = faker.random.words() 20 | const repeatedName = faker.random.words() 21 | 22 | chai.use(chaiHttp) 23 | 24 | describe('*********** CITIES ***********', () => { 25 | describe('/POST login', () => { 26 | it('it should GET token', (done) => { 27 | chai 28 | .request(server) 29 | .post('/login') 30 | .send(loginDetails) 31 | .end((err, res) => { 32 | res.should.have.status(200) 33 | res.body.should.be.an('object') 34 | res.body.should.have.property('token') 35 | token = res.body.token 36 | done() 37 | }) 38 | }) 39 | }) 40 | 41 | describe('/GET cities', () => { 42 | it('it should NOT be able to consume the route since no token was sent', (done) => { 43 | chai 44 | .request(server) 45 | .get('/cities') 46 | .end((err, res) => { 47 | res.should.have.status(401) 48 | done() 49 | }) 50 | }) 51 | it('it should GET all the cities', (done) => { 52 | chai 53 | .request(server) 54 | .get('/cities') 55 | .set('Authorization', `Bearer ${token}`) 56 | .end((err, res) => { 57 | res.should.have.status(200) 58 | res.body.should.be.an('object') 59 | res.body.docs.should.be.a('array') 60 | done() 61 | }) 62 | }) 63 | it('it should GET the cities with filters', (done) => { 64 | chai 65 | .request(server) 66 | .get('/cities?filter=Bucaramanga&fields=name') 67 | .set('Authorization', `Bearer ${token}`) 68 | .end((err, res) => { 69 | res.should.have.status(200) 70 | res.body.should.be.an('object') 71 | res.body.docs.should.be.a('array') 72 | res.body.docs.should.have.lengthOf(1) 73 | res.body.docs[0].should.have.property('name').eql('Bucaramanga') 74 | done() 75 | }) 76 | }) 77 | }) 78 | 79 | describe('/POST city', () => { 80 | it('it should NOT POST a city without name', (done) => { 81 | const city = {} 82 | chai 83 | .request(server) 84 | .post('/cities') 85 | .set('Authorization', `Bearer ${token}`) 86 | .send(city) 87 | .end((err, res) => { 88 | res.should.have.status(422) 89 | res.body.should.be.a('object') 90 | res.body.should.have.property('errors') 91 | done() 92 | }) 93 | }) 94 | it('it should POST a city ', (done) => { 95 | const city = { 96 | name 97 | } 98 | chai 99 | .request(server) 100 | .post('/cities') 101 | .set('Authorization', `Bearer ${token}`) 102 | .send(city) 103 | .end((err, res) => { 104 | res.should.have.status(201) 105 | res.body.should.be.a('object') 106 | res.body.should.include.keys('_id', 'name') 107 | createdID.push(res.body._id) 108 | done() 109 | }) 110 | }) 111 | it('it should NOT POST a city that already exists', (done) => { 112 | const city = { 113 | name 114 | } 115 | chai 116 | .request(server) 117 | .post('/cities') 118 | .set('Authorization', `Bearer ${token}`) 119 | .send(city) 120 | .end((err, res) => { 121 | res.should.have.status(422) 122 | res.body.should.be.a('object') 123 | res.body.should.have.property('errors') 124 | done() 125 | }) 126 | }) 127 | }) 128 | 129 | describe('/GET/:id city', () => { 130 | it('it should GET a city by the given id', (done) => { 131 | const id = createdID.slice(-1).pop() 132 | chai 133 | .request(server) 134 | .get(`/cities/${id}`) 135 | .set('Authorization', `Bearer ${token}`) 136 | .end((error, res) => { 137 | res.should.have.status(200) 138 | res.body.should.be.a('object') 139 | res.body.should.have.property('name') 140 | res.body.should.have.property('_id').eql(id) 141 | done() 142 | }) 143 | }) 144 | }) 145 | 146 | describe('/PATCH/:id city', () => { 147 | it('it should UPDATE a city given the id', (done) => { 148 | const id = createdID.slice(-1).pop() 149 | chai 150 | .request(server) 151 | .patch(`/cities/${id}`) 152 | .set('Authorization', `Bearer ${token}`) 153 | .send({ 154 | name: newName 155 | }) 156 | .end((error, res) => { 157 | res.should.have.status(200) 158 | res.body.should.be.a('object') 159 | res.body.should.have.property('_id').eql(id) 160 | res.body.should.have.property('name').eql(newName) 161 | done() 162 | }) 163 | }) 164 | it('it should NOT UPDATE a city that already exists', (done) => { 165 | const city = { 166 | name: repeatedName 167 | } 168 | chai 169 | .request(server) 170 | .post('/cities') 171 | .set('Authorization', `Bearer ${token}`) 172 | .send(city) 173 | .end((err, res) => { 174 | res.should.have.status(201) 175 | res.body.should.be.a('object') 176 | res.body.should.include.keys('_id', 'name') 177 | res.body.should.have.property('name').eql(repeatedName) 178 | createdID.push(res.body._id) 179 | const anotherCity = { 180 | name: newName 181 | } 182 | chai 183 | .request(server) 184 | .patch(`/cities/${createdID.slice(-1).pop()}`) 185 | .set('Authorization', `Bearer ${token}`) 186 | .send(anotherCity) 187 | .end((error, result) => { 188 | result.should.have.status(422) 189 | result.body.should.be.a('object') 190 | result.body.should.have.property('errors') 191 | done() 192 | }) 193 | }) 194 | }) 195 | }) 196 | 197 | describe('/DELETE/:id city', () => { 198 | it('it should DELETE a city given the id', (done) => { 199 | const city = { 200 | name 201 | } 202 | chai 203 | .request(server) 204 | .post('/cities') 205 | .set('Authorization', `Bearer ${token}`) 206 | .send(city) 207 | .end((err, res) => { 208 | res.should.have.status(201) 209 | res.body.should.be.a('object') 210 | res.body.should.include.keys('_id', 'name') 211 | res.body.should.have.property('name').eql(name) 212 | chai 213 | .request(server) 214 | .delete(`/cities/${res.body._id}`) 215 | .set('Authorization', `Bearer ${token}`) 216 | .end((error, result) => { 217 | result.should.have.status(200) 218 | result.body.should.be.a('object') 219 | result.body.should.have.property('msg').eql('DELETED') 220 | done() 221 | }) 222 | }) 223 | }) 224 | }) 225 | 226 | after(() => { 227 | createdID.forEach((id) => { 228 | City.findByIdAndRemove(id, (err) => { 229 | if (err) { 230 | console.log(err) 231 | } 232 | }) 233 | }) 234 | }) 235 | }) 236 | -------------------------------------------------------------------------------- /test/profile.js: -------------------------------------------------------------------------------- 1 | /* eslint handle-callback-err: "off"*/ 2 | 3 | process.env.NODE_ENV = 'test' 4 | 5 | const chai = require('chai') 6 | const chaiHttp = require('chai-http') 7 | const server = require('../server') 8 | // eslint-disable-next-line no-unused-vars 9 | const should = chai.should() 10 | const loginDetails = { 11 | email: 'admin@admin.com', 12 | password: '12345' 13 | } 14 | let token = '' 15 | 16 | chai.use(chaiHttp) 17 | 18 | describe('*********** PROFILE ***********', () => { 19 | describe('/POST login', () => { 20 | it('it should GET token', (done) => { 21 | chai 22 | .request(server) 23 | .post('/login') 24 | .send(loginDetails) 25 | .end((err, res) => { 26 | res.should.have.status(200) 27 | res.body.should.be.an('object') 28 | res.body.should.have.property('token') 29 | token = res.body.token 30 | done() 31 | }) 32 | }) 33 | }) 34 | describe('/GET profile', () => { 35 | it('it should NOT be able to consume the route since no token was sent', (done) => { 36 | chai 37 | .request(server) 38 | .get('/profile') 39 | .end((err, res) => { 40 | res.should.have.status(401) 41 | done() 42 | }) 43 | }) 44 | it('it should GET profile', (done) => { 45 | chai 46 | .request(server) 47 | .get('/profile') 48 | .set('Authorization', `Bearer ${token}`) 49 | .end((err, res) => { 50 | res.should.have.status(200) 51 | res.body.should.be.an('object') 52 | res.body.should.include.keys('name', 'email') 53 | done() 54 | }) 55 | }) 56 | }) 57 | describe('/PATCH profile', () => { 58 | it('it should NOT UPDATE profile empty name/email', (done) => { 59 | const user = {} 60 | chai 61 | .request(server) 62 | .patch('/profile') 63 | .set('Authorization', `Bearer ${token}`) 64 | .send(user) 65 | .end((err, res) => { 66 | res.should.have.status(422) 67 | res.body.should.be.a('object') 68 | res.body.should.have.property('errors') 69 | done() 70 | }) 71 | }) 72 | it('it should UPDATE profile', (done) => { 73 | const user = { 74 | name: 'Test123456', 75 | urlTwitter: 'https://hello.com', 76 | urlGitHub: 'https://hello.io', 77 | phone: '123123123', 78 | city: 'Bucaramanga', 79 | country: 'Colombia' 80 | } 81 | chai 82 | .request(server) 83 | .patch('/profile') 84 | .set('Authorization', `Bearer ${token}`) 85 | .send(user) 86 | .end((err, res) => { 87 | res.should.have.status(200) 88 | res.body.should.be.a('object') 89 | res.body.should.have.property('name').eql('Test123456') 90 | res.body.should.have.property('urlTwitter').eql('https://hello.com') 91 | res.body.should.have.property('urlGitHub').eql('https://hello.io') 92 | res.body.should.have.property('phone').eql('123123123') 93 | res.body.should.have.property('city').eql('Bucaramanga') 94 | res.body.should.have.property('country').eql('Colombia') 95 | done() 96 | }) 97 | }) 98 | it('it should NOT UPDATE profile with email that already exists', (done) => { 99 | const user = { 100 | email: 'programmer@programmer.com' 101 | } 102 | chai 103 | .request(server) 104 | .patch('/profile') 105 | .set('Authorization', `Bearer ${token}`) 106 | .send(user) 107 | .end((err, res) => { 108 | res.should.have.status(422) 109 | res.body.should.be.a('object') 110 | res.body.should.have.property('errors') 111 | done() 112 | }) 113 | }) 114 | it('it should NOT UPDATE profile with not valid URL´s', (done) => { 115 | const user = { 116 | name: 'Test123456', 117 | urlTwitter: 'hello', 118 | urlGitHub: 'hello', 119 | phone: '123123123', 120 | city: 'Bucaramanga', 121 | country: 'Colombia' 122 | } 123 | chai 124 | .request(server) 125 | .patch('/profile') 126 | .set('Authorization', `Bearer ${token}`) 127 | .send(user) 128 | .end((err, res) => { 129 | res.should.have.status(422) 130 | res.body.should.be.a('object') 131 | res.body.should.have.property('errors').that.has.property('msg') 132 | res.body.errors.msg[0].should.have 133 | .property('msg') 134 | .eql('NOT_A_VALID_URL') 135 | done() 136 | }) 137 | }) 138 | }) 139 | describe('/POST profile/changePassword', () => { 140 | it('it should NOT change password', (done) => { 141 | const data = { 142 | oldPassword: '123456', 143 | newPassword: '123456' 144 | } 145 | chai 146 | .request(server) 147 | .post('/profile/changePassword') 148 | .set('Authorization', `Bearer ${token}`) 149 | .send(data) 150 | .end((err, res) => { 151 | res.should.have.status(409) 152 | res.body.should.be.a('object') 153 | res.body.should.have 154 | .property('errors') 155 | .that.has.property('msg') 156 | .eql('WRONG_PASSWORD') 157 | done() 158 | }) 159 | }) 160 | it('it should NOT change a too short password', (done) => { 161 | const data = { 162 | oldPassword: '1234', 163 | newPassword: '1234' 164 | } 165 | chai 166 | .request(server) 167 | .post('/profile/changePassword') 168 | .set('Authorization', `Bearer ${token}`) 169 | .send(data) 170 | .end((err, res) => { 171 | res.should.have.status(422) 172 | res.body.should.be.a('object') 173 | res.body.should.have.property('errors').that.has.property('msg') 174 | res.body.errors.msg[0].should.have 175 | .property('msg') 176 | .eql('PASSWORD_TOO_SHORT_MIN_5') 177 | done() 178 | }) 179 | }) 180 | it('it should change password', (done) => { 181 | const data = { 182 | oldPassword: '12345', 183 | newPassword: '12345' 184 | } 185 | chai 186 | .request(server) 187 | .post('/profile/changePassword') 188 | .set('Authorization', `Bearer ${token}`) 189 | .send(data) 190 | .end((err, res) => { 191 | res.should.have.status(200) 192 | res.body.should.be.a('object') 193 | res.body.should.have.property('msg').eql('PASSWORD_CHANGED') 194 | done() 195 | }) 196 | }) 197 | }) 198 | }) 199 | -------------------------------------------------------------------------------- /test/users.js: -------------------------------------------------------------------------------- 1 | /* eslint handle-callback-err: "off"*/ 2 | 3 | process.env.NODE_ENV = 'test' 4 | 5 | const User = require('../app/models/user') 6 | const faker = require('faker') 7 | const chai = require('chai') 8 | const chaiHttp = require('chai-http') 9 | const server = require('../server') 10 | // eslint-disable-next-line no-unused-vars 11 | const should = chai.should() 12 | const loginDetails = { 13 | admin: { 14 | id: '5aa1c2c35ef7a4e97b5e995a', 15 | email: 'admin@admin.com', 16 | password: '12345' 17 | }, 18 | user: { 19 | id: '5aa1c2c35ef7a4e97b5e995b', 20 | email: 'user@user.com', 21 | password: '12345' 22 | } 23 | } 24 | const tokens = { 25 | admin: '', 26 | user: '' 27 | } 28 | 29 | const email = faker.internet.email() 30 | const createdID = [] 31 | 32 | chai.use(chaiHttp) 33 | 34 | describe('*********** USERS ***********', () => { 35 | describe('/POST login', () => { 36 | it('it should GET token as admin', (done) => { 37 | chai 38 | .request(server) 39 | .post('/login') 40 | .send(loginDetails.admin) 41 | .end((err, res) => { 42 | res.should.have.status(200) 43 | res.body.should.be.an('object') 44 | res.body.should.have.property('token') 45 | tokens.admin = res.body.token 46 | done() 47 | }) 48 | }) 49 | it('it should GET token as user', (done) => { 50 | chai 51 | .request(server) 52 | .post('/login') 53 | .send(loginDetails.user) 54 | .end((err, res) => { 55 | res.should.have.status(200) 56 | res.body.should.be.an('object') 57 | res.body.should.have.property('token') 58 | tokens.user = res.body.token 59 | done() 60 | }) 61 | }) 62 | }) 63 | describe('/GET users', () => { 64 | it('it should NOT be able to consume the route since no token was sent', (done) => { 65 | chai 66 | .request(server) 67 | .get('/users') 68 | .end((err, res) => { 69 | res.should.have.status(401) 70 | done() 71 | }) 72 | }) 73 | it('it should GET all the users', (done) => { 74 | chai 75 | .request(server) 76 | .get('/users') 77 | .set('Authorization', `Bearer ${tokens.admin}`) 78 | .end((err, res) => { 79 | res.should.have.status(200) 80 | res.body.should.be.an('object') 81 | res.body.docs.should.be.a('array') 82 | done() 83 | }) 84 | }) 85 | it('it should GET the users with filters', (done) => { 86 | chai 87 | .request(server) 88 | .get('/users?filter=admin&fields=name,email,city,country,phone') 89 | .set('Authorization', `Bearer ${tokens.admin}`) 90 | .end((err, res) => { 91 | res.should.have.status(200) 92 | res.body.should.be.an('object') 93 | res.body.docs.should.be.a('array') 94 | res.body.docs.should.have.lengthOf(1) 95 | res.body.docs[0].should.have.property('email').eql('admin@admin.com') 96 | done() 97 | }) 98 | }) 99 | }) 100 | describe('/POST user', () => { 101 | it('it should NOT POST a user without name', (done) => { 102 | const user = {} 103 | chai 104 | .request(server) 105 | .post('/users') 106 | .set('Authorization', `Bearer ${tokens.admin}`) 107 | .send(user) 108 | .end((err, res) => { 109 | res.should.have.status(422) 110 | res.body.should.be.a('object') 111 | res.body.should.have.property('errors') 112 | done() 113 | }) 114 | }) 115 | it('it should POST a user ', (done) => { 116 | const user = { 117 | name: faker.random.words(), 118 | email, 119 | password: faker.random.words(), 120 | role: 'admin', 121 | urlTwitter: faker.internet.url(), 122 | urlGitHub: faker.internet.url(), 123 | phone: faker.phone.phoneNumber(), 124 | city: faker.random.words(), 125 | country: faker.random.words() 126 | } 127 | chai 128 | .request(server) 129 | .post('/users') 130 | .set('Authorization', `Bearer ${tokens.admin}`) 131 | .send(user) 132 | .end((err, res) => { 133 | res.should.have.status(201) 134 | res.body.should.be.a('object') 135 | res.body.should.include.keys('_id', 'name', 'email', 'verification') 136 | createdID.push(res.body._id) 137 | done() 138 | }) 139 | }) 140 | it('it should NOT POST a user with email that already exists', (done) => { 141 | const user = { 142 | name: faker.random.words(), 143 | email, 144 | password: faker.random.words(), 145 | role: 'admin' 146 | } 147 | chai 148 | .request(server) 149 | .post('/users') 150 | .set('Authorization', `Bearer ${tokens.admin}`) 151 | .send(user) 152 | .end((err, res) => { 153 | res.should.have.status(422) 154 | res.body.should.be.a('object') 155 | res.body.should.have.property('errors') 156 | done() 157 | }) 158 | }) 159 | it('it should NOT POST a user with not known role', (done) => { 160 | const user = { 161 | name: faker.random.words(), 162 | email, 163 | password: faker.random.words(), 164 | role: faker.random.words() 165 | } 166 | chai 167 | .request(server) 168 | .post('/users') 169 | .set('Authorization', `Bearer ${tokens.admin}`) 170 | .send(user) 171 | .end((err, res) => { 172 | res.should.have.status(422) 173 | res.body.should.be.a('object') 174 | res.body.should.have.property('errors') 175 | done() 176 | }) 177 | }) 178 | }) 179 | describe('/GET/:id user', () => { 180 | it('it should GET a user by the given id', (done) => { 181 | const id = createdID.slice(-1).pop() 182 | chai 183 | .request(server) 184 | .get(`/users/${id}`) 185 | .set('Authorization', `Bearer ${tokens.admin}`) 186 | .end((error, res) => { 187 | res.should.have.status(200) 188 | res.body.should.be.a('object') 189 | res.body.should.have.property('name') 190 | res.body.should.have.property('_id').eql(id) 191 | done() 192 | }) 193 | }) 194 | }) 195 | describe('/PATCH/:id user', () => { 196 | it('it should UPDATE a user given the id', (done) => { 197 | const id = createdID.slice(-1).pop() 198 | const user = { 199 | name: 'JS123456', 200 | email: 'emailthatalreadyexists@email.com', 201 | role: 'admin', 202 | urlTwitter: faker.internet.url(), 203 | urlGitHub: faker.internet.url(), 204 | phone: faker.phone.phoneNumber(), 205 | city: faker.random.words(), 206 | country: faker.random.words() 207 | } 208 | chai 209 | .request(server) 210 | .patch(`/users/${id}`) 211 | .set('Authorization', `Bearer ${tokens.admin}`) 212 | .send(user) 213 | .end((error, res) => { 214 | res.should.have.status(200) 215 | res.body.should.be.a('object') 216 | res.body.should.have.property('_id').eql(id) 217 | res.body.should.have.property('name').eql('JS123456') 218 | res.body.should.have 219 | .property('email') 220 | .eql('emailthatalreadyexists@email.com') 221 | createdID.push(res.body._id) 222 | done() 223 | }) 224 | }) 225 | it('it should NOT UPDATE a user with email that already exists', (done) => { 226 | const id = createdID.slice(-1).pop() 227 | const user = { 228 | name: faker.random.words(), 229 | email: 'admin@admin.com', 230 | role: 'admin' 231 | } 232 | chai 233 | .request(server) 234 | .patch(`/users/${id}`) 235 | .set('Authorization', `Bearer ${tokens.admin}`) 236 | .send(user) 237 | .end((err, res) => { 238 | res.should.have.status(422) 239 | res.body.should.be.a('object') 240 | res.body.should.have.property('errors') 241 | done() 242 | }) 243 | }) 244 | it('it should NOT UPDATE another user if not an admin', (done) => { 245 | const id = createdID.slice(-1).pop() 246 | const user = { 247 | name: faker.random.words(), 248 | email: 'toto@toto.com', 249 | role: 'user' 250 | } 251 | chai 252 | .request(server) 253 | .patch(`/users/${id}`) 254 | .set('Authorization', `Bearer ${tokens.user}`) 255 | .send(user) 256 | .end((err, res) => { 257 | res.should.have.status(401) 258 | res.body.should.be.a('object') 259 | res.body.should.have.property('errors') 260 | done() 261 | }) 262 | }) 263 | }) 264 | describe('/DELETE/:id user', () => { 265 | it('it should DELETE a user given the id', (done) => { 266 | const user = { 267 | name: faker.random.words(), 268 | email: faker.internet.email(), 269 | password: faker.random.words(), 270 | role: 'admin', 271 | urlTwitter: faker.internet.url(), 272 | urlGitHub: faker.internet.url(), 273 | phone: faker.phone.phoneNumber(), 274 | city: faker.random.words(), 275 | country: faker.random.words() 276 | } 277 | chai 278 | .request(server) 279 | .post('/users') 280 | .set('Authorization', `Bearer ${tokens.admin}`) 281 | .send(user) 282 | .end((err, res) => { 283 | res.should.have.status(201) 284 | res.body.should.be.a('object') 285 | res.body.should.include.keys('_id', 'name', 'email', 'verification') 286 | chai 287 | .request(server) 288 | .delete(`/users/${res.body._id}`) 289 | .set('Authorization', `Bearer ${tokens.admin}`) 290 | .end((error, result) => { 291 | result.should.have.status(200) 292 | result.body.should.be.a('object') 293 | result.body.should.have.property('msg').eql('DELETED') 294 | done() 295 | }) 296 | }) 297 | }) 298 | }) 299 | 300 | after(() => { 301 | createdID.forEach((id) => { 302 | User.findByIdAndRemove(id, (err) => { 303 | if (err) { 304 | console.log(err) 305 | } 306 | }) 307 | }) 308 | }) 309 | }) 310 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | node-express-mongodb-jwt-rest-api-skeleton 9 | 10 | 11 | 12 |

API home

13 |

For more info visit: node-express-mongodb-jwt-rest-api-skeleton

14 | 15 | 16 | 17 | --------------------------------------------------------------------------------