├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── bin └── server.js ├── cert ├── localhost.cert └── localhost.key ├── config ├── env │ ├── common.js │ ├── development.js │ ├── production.js │ └── test.js ├── index.js └── passport.js ├── docker-compose.yml ├── index.js ├── package-lock.json ├── package.json ├── pm2.config.js ├── prestart.sh ├── src ├── middleware │ ├── index.js │ └── validators.js ├── models │ └── users.js ├── modules │ ├── common │ │ ├── home │ │ │ ├── controller.js │ │ │ └── router.js │ │ └── index.js │ ├── v1 │ │ ├── auth │ │ │ ├── controller.js │ │ │ └── router.js │ │ ├── index.js │ │ └── users │ │ │ ├── controller.js │ │ │ └── router.js │ └── v2 │ │ ├── auth │ │ ├── controller.js │ │ └── router.js │ │ ├── index.js │ │ └── users │ │ ├── controller.js │ │ └── router.js ├── requestModel │ ├── v1 │ │ ├── auth.js │ │ └── users.js │ └── v2 │ │ ├── auth.js │ │ └── users.js ├── responseModel │ ├── v1 │ │ ├── auth.js │ │ └── users.js │ └── v2 │ │ ├── auth.js │ │ └── users.js └── utils │ ├── auth.js │ └── constants.js ├── static ├── Average.png ├── Throughput.png └── koach.png └── test ├── auth.spec.js ├── users.spec.js └── utils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015-node5", 4 | "stage-0" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/cert/** 3 | swagger 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | node_modules 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | 38 | 39 | ### SublimeText ### 40 | # cache files for sublime text 41 | *.tmlanguage.cache 42 | *.tmPreferences.cache 43 | *.stTheme.cache 44 | 45 | # workspace files are user-specific 46 | *.sublime-workspace 47 | 48 | # project files should be checked into the repository, unless a significant 49 | # proportion of contributors will probably not be using SublimeText 50 | *.sublime-project 51 | 52 | # sftp configuration file 53 | sftp-config.json 54 | 55 | #Documentation 56 | docs 57 | .env 58 | .vscode -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 8, 4 | "ecmaFeatures": { 5 | "experimentalObjectRestSpread": true, 6 | "jsx": true 7 | }, 8 | "sourceType": "module" 9 | }, 10 | "env": { 11 | "es6": true, 12 | "node": true 13 | }, 14 | 15 | "plugins": [ 16 | "import", 17 | "node", 18 | "promise", 19 | "standard" 20 | ], 21 | 22 | "globals": { 23 | "document": false, 24 | "navigator": false, 25 | "window": false 26 | }, 27 | "rules": { 28 | "accessor-pairs": "error", 29 | "arrow-spacing": [ 30 | "error", 31 | { 32 | "before": true, 33 | "after": true 34 | } 35 | ], 36 | "block-spacing": [ 37 | "error", 38 | "always" 39 | ], 40 | "brace-style": [ 41 | "error", 42 | "1tbs", 43 | { 44 | "allowSingleLine": true 45 | } 46 | ], 47 | "camelcase": [ 48 | "error", 49 | { 50 | "properties": "never" 51 | } 52 | ], 53 | "comma-dangle": [ 54 | "error", 55 | { 56 | "arrays": "never", 57 | "objects": "never", 58 | "imports": "never", 59 | "exports": "never", 60 | "functions": "never" 61 | } 62 | ], 63 | "comma-spacing": [ 64 | "error", 65 | { 66 | "before": false, 67 | "after": true 68 | } 69 | ], 70 | "comma-style": [ 71 | "error", 72 | "last" 73 | ], 74 | "curly": [ 75 | "error", 76 | "multi-line" 77 | ], 78 | "dot-location": [ 79 | "error", 80 | "property" 81 | ], 82 | "eol-last": "error", 83 | "eqeqeq": [ 84 | "error", 85 | "always", 86 | { 87 | "null": "ignore" 88 | } 89 | ], 90 | "func-call-spacing": [ 91 | "error", 92 | "never" 93 | ], 94 | "generator-star-spacing": [ 95 | "error", 96 | { 97 | "before": true, 98 | "after": true 99 | } 100 | ], 101 | "handle-callback-err": [ 102 | "error", 103 | "^(err|error)$" 104 | ], 105 | "indent": [ 106 | "error", 107 | "tab", 108 | { 109 | "SwitchCase": 1 110 | } 111 | ], 112 | "key-spacing": [ 113 | "error", 114 | { 115 | "beforeColon": false, 116 | "afterColon": true 117 | } 118 | ], 119 | "new-cap": [ 120 | "error", 121 | { 122 | "newIsCap": true, 123 | "capIsNew": false 124 | } 125 | ], 126 | "new-parens": "error", 127 | "no-array-constructor": "error", 128 | "no-caller": "error", 129 | "no-class-assign": "error", 130 | "no-compare-neg-zero": "error", 131 | "no-cond-assign": "error", 132 | "no-const-assign": "error", 133 | "no-constant-condition": [ 134 | "error", 135 | { 136 | "checkLoops": false 137 | } 138 | ], 139 | "no-control-regex": "error", 140 | "no-debugger": "error", 141 | "no-delete-var": "error", 142 | "no-dupe-args": "error", 143 | "no-dupe-class-members": "error", 144 | "no-dupe-keys": "error", 145 | "no-duplicate-case": "error", 146 | "no-empty-character-class": "error", 147 | "no-empty-pattern": "error", 148 | "no-eval": "error", 149 | "no-ex-assign": "error", 150 | "no-extend-native": "error", 151 | "no-extra-bind": "error", 152 | "no-extra-boolean-cast": "error", 153 | "no-extra-parens": [ 154 | "error", 155 | "functions" 156 | ], 157 | "no-fallthrough": "error", 158 | "no-floating-decimal": "error", 159 | "no-func-assign": "error", 160 | "no-global-assign": "error", 161 | "no-implied-eval": "error", 162 | "no-inner-declarations": [ 163 | "error", 164 | "functions" 165 | ], 166 | "no-invalid-regexp": "error", 167 | "no-irregular-whitespace": "error", 168 | "no-iterator": "error", 169 | "no-label-var": "error", 170 | "no-labels": [ 171 | "error", 172 | { 173 | "allowLoop": false, 174 | "allowSwitch": false 175 | } 176 | ], 177 | "no-lone-blocks": "error", 178 | "no-mixed-operators": [ 179 | "error", 180 | { 181 | "groups": [ 182 | [ 183 | "==", 184 | "!=", 185 | "===", 186 | "!==", 187 | ">", 188 | ">=", 189 | "<", 190 | "<=" 191 | ], 192 | [ 193 | "&&", 194 | "||" 195 | ], 196 | [ 197 | "in", 198 | "instanceof" 199 | ] 200 | ], 201 | "allowSamePrecedence": true 202 | } 203 | ], 204 | "no-mixed-spaces-and-tabs": "error", 205 | "no-multi-spaces": "error", 206 | "no-multi-str": "error", 207 | "no-multiple-empty-lines": [ 208 | "error", 209 | { 210 | "max": 1, 211 | "maxEOF": 0 212 | } 213 | ], 214 | "no-negated-in-lhs": "error", 215 | "no-new": "error", 216 | "no-new-func": "error", 217 | "no-new-object": "error", 218 | "no-new-require": "error", 219 | "no-new-symbol": "error", 220 | "no-new-wrappers": "error", 221 | "no-obj-calls": "error", 222 | "no-octal": "error", 223 | "no-octal-escape": "error", 224 | "no-path-concat": "error", 225 | "no-proto": "error", 226 | "no-redeclare": "error", 227 | "no-regex-spaces": "error", 228 | "no-return-assign": [ 229 | "error", 230 | "except-parens" 231 | ], 232 | "no-return-await": "error", 233 | "no-self-assign": "error", 234 | "no-self-compare": "error", 235 | "no-sequences": "error", 236 | "no-shadow-restricted-names": "error", 237 | "no-sparse-arrays": "error", 238 | "no-template-curly-in-string": "error", 239 | "no-this-before-super": "error", 240 | "no-throw-literal": "error", 241 | "no-trailing-spaces": "error", 242 | "no-undef": "error", 243 | "no-undef-init": "error", 244 | "no-unexpected-multiline": "error", 245 | "no-unmodified-loop-condition": "error", 246 | "no-unneeded-ternary": [ 247 | "error", 248 | { 249 | "defaultAssignment": false 250 | } 251 | ], 252 | "no-unreachable": "error", 253 | "no-unsafe-finally": "error", 254 | "no-unsafe-negation": "error", 255 | "no-unused-expressions": [ 256 | "error", 257 | { 258 | "allowShortCircuit": true, 259 | "allowTernary": true, 260 | "allowTaggedTemplates": true 261 | } 262 | ], 263 | "no-unused-vars": [ 264 | "error", 265 | { 266 | "vars": "all", 267 | "args": "none", 268 | "ignoreRestSiblings": true 269 | } 270 | ], 271 | "no-use-before-define": [ 272 | "error", 273 | { 274 | "functions": false, 275 | "classes": false, 276 | "variables": false 277 | } 278 | ], 279 | "no-useless-call": "error", 280 | "no-useless-computed-key": "error", 281 | "no-useless-constructor": "error", 282 | "no-useless-escape": "error", 283 | "no-useless-rename": "error", 284 | "no-useless-return": "error", 285 | "no-whitespace-before-property": "error", 286 | "no-with": "error", 287 | "object-property-newline": [ 288 | "error", 289 | { 290 | "allowMultiplePropertiesPerLine": true 291 | } 292 | ], 293 | "one-var": [ 294 | "error", 295 | { 296 | "initialized": "never" 297 | } 298 | ], 299 | "operator-linebreak": [ 300 | "error", 301 | "after", 302 | { 303 | "overrides": { 304 | "?": "before", 305 | ":": "before" 306 | } 307 | } 308 | ], 309 | "quotes": [ 310 | "error", 311 | "single", 312 | { 313 | "avoidEscape": true, 314 | "allowTemplateLiterals": true 315 | } 316 | ], 317 | "rest-spread-spacing": [ 318 | "error", 319 | "never" 320 | ], 321 | "semi-spacing": [ 322 | "error", 323 | { 324 | "before": false, 325 | "after": true 326 | } 327 | ], 328 | "space-before-blocks": [ 329 | "error", 330 | "always" 331 | ], 332 | "space-in-parens": [ 333 | "error", 334 | "never" 335 | ], 336 | "space-unary-ops": [ 337 | "error", 338 | { 339 | "words": true, 340 | "nonwords": false 341 | } 342 | ], 343 | "spaced-comment": [ 344 | "error", 345 | "always", 346 | { 347 | "line": { 348 | "markers": [ 349 | "*package", 350 | "!", 351 | "/", 352 | "," 353 | ] 354 | }, 355 | "block": { 356 | "balanced": true, 357 | "markers": [ 358 | "*package", 359 | "!", 360 | ",", 361 | ":", 362 | "::", 363 | "flow-include" 364 | ], 365 | "exceptions": [ 366 | "*" 367 | ] 368 | } 369 | } 370 | ], 371 | "symbol-description": "error", 372 | "template-curly-spacing": [ 373 | "error", 374 | "never" 375 | ], 376 | "template-tag-spacing": [ 377 | "error", 378 | "never" 379 | ], 380 | "unicode-bom": [ 381 | "error", 382 | "never" 383 | ], 384 | "use-isnan": "error", 385 | "valid-typeof": [ 386 | "error", 387 | { 388 | "requireStringLiterals": true 389 | } 390 | ], 391 | "wrap-iife": [ 392 | "error", 393 | "any", 394 | { 395 | "functionPrototypeMethods": true 396 | } 397 | ], 398 | "yield-star-spacing": [ 399 | "error", 400 | "both" 401 | ], 402 | "yoda": [ 403 | "error", 404 | "never" 405 | ], 406 | "import/export": "error", 407 | "import/first": "error", 408 | "import/no-duplicates": "error", 409 | "import/no-webpack-loader-syntax": "error", 410 | "node/no-deprecated-api": "error", 411 | "node/process-exit-as-throw": "error", 412 | "promise/param-names": "error", 413 | "standard/array-bracket-even-spacing": [ 414 | "error", 415 | "either" 416 | ], 417 | "standard/computed-property-even-spacing": [ 418 | "error", 419 | "even" 420 | ], 421 | "standard/no-callback-literal": "error", 422 | "standard/object-curly-even-spacing": [ 423 | "error", 424 | "either" 425 | ] 426 | } 427 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,sublimetext 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | node_modules 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | 38 | 39 | ### SublimeText ### 40 | # cache files for sublime text 41 | *.tmlanguage.cache 42 | *.tmPreferences.cache 43 | *.stTheme.cache 44 | 45 | # workspace files are user-specific 46 | *.sublime-workspace 47 | 48 | # project files should be checked into the repository, unless a significant 49 | # proportion of contributors will probably not be using SublimeText 50 | *.sublime-project 51 | 52 | # sftp configuration file 53 | sftp-config.json 54 | 55 | #Documentation 56 | docs 57 | .env 58 | .vscode 59 | 60 | # Mac 61 | .DS_Store -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.16.1-alpine 2 | 3 | WORKDIR /src 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | RUN npm install -g pm2 10 | 11 | COPY . . 12 | 13 | CMD ["pm2-runtime", "pm2.config.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Arpit Khandelwal 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![KOACH](https://github.com/SystangoTechnologies/Koach/blob/master/static/koach.png) 2 | 3 | ## KOACH 4 | Production ready boilerplate for building APIs with [koa2](https://github.com/koajs/koa/) using mongodb as the database and http/2 as the communication protocol. 5 | 6 | ## Description 7 | This project covers basic necessities of most APIs. 8 | * Authentication (passport & jwt) 9 | * Database (mongoose) 10 | * Testing (mocha) 11 | * http/2 support for websites and apis 12 | * Doc generation with apidoc 13 | * linting using standard 14 | * Contains two versions of API 15 | 16 | Please note, if you are planning to use this boilerplate for creating a web application then the 'https' protocol should be used to access its pages. 17 | 18 | Visit `https://localhost:3000/` to access the root page. 19 | 20 | ## Requirements 21 | * node = v10.15.3 22 | * MongoDB 23 | 24 | ## .env Configuration 25 | To simulate environment variables in Dev environment, please create .env file at the root location and define the following properties - 26 | 27 | ``` 28 | NODE_ENV=production // Node environment development/production 29 | PORT=8000 // Server Port 30 | SESSION=0.0.0.0 // secret-boilerplate-token 31 | JWT_SECRET=This_Should_be_32_characters_long // Jwt secret 32 | DATABASE_URL=mongodb://localhost:27017/koach-dev // Mongo database url 33 | #DATABASE_URL=mongodb://mongodb:27017/koach-dev // Mongo database url while using docker 34 | DB_PATH=/home/ulap28/work/systango-frameworks // path where database volumen mounted. 35 | 36 | ## Installation 37 | ```bash 38 | git clone https://github.com/SystangoTechnologies/Koach.git 39 | ``` 40 | 41 | ## Features 42 | * [koa2](https://github.com/koajs/koa) 43 | * [koa-router](https://github.com/alexmingoia/koa-router) 44 | * [koa-bodyparser](https://github.com/koajs/bodyparser) 45 | * [koa-generic-session](https://github.com/koajs/generic-session) 46 | * [koa-logger](https://github.com/koajs/logger) 47 | * [koa-helmet](https://github.com/venables/koa-helmet) 48 | * [koa-convert](https://github.com/koajs/convert) 49 | * [MongoDB](http://mongodb.org/) 50 | * [Mongoose](http://mongoosejs.com/) 51 | * [http/2](https://github.com/molnarg/node-http2) 52 | * [Passport](http://passportjs.org/) 53 | * [Nodemon](http://nodemon.io/) 54 | * [Mocha](https://mochajs.org/) 55 | * [apidoc](http://apidocjs.com/) 56 | * [Babel](https://github.com/babel/babel) 57 | * [ESLint](http://eslint.org/) 58 | * [PM2](https://github.com/Unitech/pm2/) 59 | * [Swagger](https://github.com/SystangoTechnologies/swagger-generator-koa/blob/master/README.md) 60 | 61 | ## Structure 62 | ``` 63 | ├── bin 64 | │ └── server.js # Bootstrapping and entry point 65 | ├── cert 66 | │ ├── server.cert # SSL certificate 67 | │ └── server.key # SSL certificate key 68 | ├── config # Server configuration settings 69 | │ ├── env # Environment specific config 70 | │ │ ├── common.js 71 | │ │ ├── development.js 72 | │ │ ├── production.js 73 | │ │ └── test.js 74 | │ ├── index.js # Config entrypoint 75 | │ └── passport.js # Passportjs config of strategies 76 | ├── src # Source code 77 | │ ├── modules # Module-specific controllers 78 | │ │ ├── common # Contains common modules 79 | │ │ │ ├─── home 80 | │ │ │ └─ index.js 81 | │ │ ├── v1 # Version 1 of APIs 82 | │ │ │ ├─ Auth 83 | │ │ │ ├─ User 84 | │ │ │ └─ index.js 85 | │ │ └─── v2 # Version 2 of APIs 86 | │ │ ├─ Auth 87 | │ │ ├─ User 88 | │ │ └─ index.js 89 | │ ├── models # Mongoose models 90 | | ├── requestModel 91 | | | ├── v1 92 | | | | ├── auth.js 93 | | | | └── users.js 94 | | | └── v2 95 | | | ├── auth.js 96 | | | └── users.js 97 | | ├── responseModel 98 | | | ├── v1 99 | | | | ├── auth.js 100 | | | | └── users.js 101 | | | └── v2 102 | | | ├── auth.js 103 | | | └── users.js 104 | │ └── middleware # Custom middleware 105 | │ └── validators # Validation middleware 106 | └── test # Unit tests 107 | └── pm2.config.js # PM2 configuration file 108 | └── Dockerfile # Docker file 109 | └── docker-compose.yml # Docker Compose file 110 | ``` 111 | 112 | 113 | ## Usage 114 | * `npm start` Start server on development mode with Nodemon 115 | * `npm run prod` Start server on production mode with PM2 116 | * `npm run docs` Generate API documentation 117 | * `npm test` Run mocha tests 118 | 119 | ## Running the server in Docker Container 120 | 121 | Prerequisite For Docker Configuration : Docker and docker compose must be installed on the system. 122 | 123 | Steps to run app in docker container : 124 | 1. CD to project dir 125 | 2. Create build using cmd: $ docker-compose build 126 | 3. Start the server in daemon thread using cmd: $ docker-compose up -d 127 | 4. Stop the server using cmd : $ docker-compose down 128 | 129 | ## Documentation 130 | API documentation is written inline and generated by [apidoc](http://apidocjs.com/). 131 | 132 | Visit [https://localhost:3000/docs/](https://localhost:3000/docs/) to view docs 133 | 134 | To view swagger API documentation 135 | 136 | Visit [https://localhost:3000/swagger](https://localhost:3000/swagger) to view Swagger UI. 137 | 138 | ## Performance Comparison 139 | The environment for the test cases are following- 140 | * Node Version: **8.9.4** 141 | * Number of Users: **1500** 142 | * Ramp-up Period: **300 seconds** 143 | * Loop Count: **100** 144 | 145 | ![Average](https://raw.githubusercontent.com/SystangoTechnologies/Koach/master/static/Average.png) 146 | ![Throughput](https://github.com/SystangoTechnologies/Koach/raw/master/static/Throughput.png) 147 | 148 | ## Contributors 149 | [Arpit Khandelwal](https://github.com/arpit-systango) 150 | [Anurag Vikram Singh](https://www.linkedin.com/in/anuragvikramsingh/) 151 | [Vikas Patidar](https://www.linkedin.com/in/vikas-patidar-0106/) 152 | [Sparsh Pipley](https://www.linkedin.com/in/sparsh-pipley-6ab0b1a4/) 153 | 154 | ## License 155 | MIT. 156 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import bodyParser from 'koa-bodyparser' 3 | import convert from 'koa-convert' 4 | import logger from 'koa-logger' 5 | import mongoose from 'mongoose' 6 | import session from 'koa-generic-session' 7 | import passport from 'koa-passport' 8 | import mount from 'koa-mount' 9 | import serve from 'koa-static' 10 | import helmet from 'koa-helmet' 11 | // import http2 from 'http2' 12 | // import fs from 'fs' 13 | import config from '../config' 14 | import { 15 | errorMiddleware 16 | } from '../src/middleware' 17 | import { 18 | serveSwagger 19 | } from 'swagger-generator-koa' 20 | 21 | const app = new Koa() 22 | app.keys = [config.session] 23 | 24 | // replace these with your certificate information 25 | // const options = { 26 | // cert: fs.readFileSync('./cert/localhost.cert'), 27 | // key: fs.readFileSync('./cert/localhost.key') 28 | // } 29 | 30 | // --------------------- start ------------------------- 31 | // Instead of calling convert for all legacy middlewares 32 | // just use the following to convert them all at once 33 | 34 | const _use = app.use 35 | app.use = x => _use.call(app, convert(x)) 36 | 37 | // The code above avoids writting the following 38 | // app.use(convert(logger())) 39 | // ---------------------- end -------------------------- 40 | 41 | mongoose.Promise = global.Promise 42 | mongoose.connect(config.database) 43 | 44 | app.use(helmet()) 45 | app.use(logger()) 46 | app.use(bodyParser()) 47 | app.use(session()) 48 | app.use(errorMiddleware()) 49 | 50 | // error handler 51 | app.use(async (ctx, next) => { 52 | try { 53 | await next(); 54 | } catch (err) { 55 | ctx.status = err.status || err.code || 500; 56 | ctx.body = { 57 | error: err.code, 58 | message: err.message, 59 | errors: err.errors 60 | }; 61 | } 62 | }); 63 | 64 | // Mount static API documents generated by api-generator 65 | app.use(mount('/docs', serve(`${process.cwd()}/docs`))) 66 | 67 | // Using Passport for authentication 68 | require('../config/passport') 69 | app.use(passport.initialize()) 70 | app.use(passport.session()) 71 | 72 | // Using module wise routing 73 | const modules1 = require('../src/modules/v1') 74 | const modules2 = require('../src/modules/v2') 75 | const common = require('../src/modules/common') 76 | 77 | Promise.all([modules1(app), modules2(app), common(app)]).then(function (values) { 78 | if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { 79 | const options = { 80 | title: 'swagger-generator-koa', 81 | version: '1.0.0', 82 | host: 'localhost:3000', 83 | basePath: '/', 84 | schemes: ['http', 'https'], 85 | securityDefinitions: { 86 | Bearer: { 87 | description: 'Example value:- Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU5MmQwMGJhNTJjYjJjM', 88 | type: 'apiKey', 89 | name: 'Authorization', 90 | in: 'header' 91 | } 92 | }, 93 | security: [{ 94 | Bearer: [] 95 | }], 96 | defaultSecurity: 'Bearer' 97 | }; 98 | serveSwagger(app, '/swagger', options, { 99 | requestModelPath: './src/requestModel', 100 | responseModelPath: './src/responseModel' 101 | }); 102 | } 103 | }).catch((error) => { 104 | throw error; 105 | }); 106 | 107 | // Using http2 to work with http/2 instead of http/1.x 108 | // http2 109 | // .createSecureServer(options, app.callback()) 110 | // .listen(config.port, () => { 111 | // console.log(`Server started on ${config.port}`) 112 | // }) 113 | 114 | app.listen(config.port, () => { 115 | console.log(`Server started on ${config.port}`) 116 | }) 117 | 118 | export default app 119 | -------------------------------------------------------------------------------- /cert/localhost.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+zCCAeOgAwIBAgIJAOQdL6as8UR7MA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xNzA0MTkwOTQ5NTBaFw0yNzA0MTcwOTQ5NTBaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAN4IOLr+LT4ACSO8eovdRoeZ0ybLsh9779N5F2gsA0gUTMXiaKZb+/wHJJYx 6 | Kv4lIghTDBaqfRE/wo1uK2+0p2vlxOCinbTp6sm7NW3aX5LfaVsDvFRlLwNbH2ld 7 | VA+lc/sLkVEWOK4twSf6enQ3rDElg5Qw5BWAurEpv6RNGGjLZb8T1mriDHihr6fG 8 | NKm2yxjPEZw0iL4oPgyqjjCIppLamMgBR/y6nxztfFBrckgcToWdL2Yb9Y/iNjwB 9 | KmsNRx0aFeXKB0fWNF6S0I1pQ24NYpjbOTilC5adHg1mfky1tKRyPYXSSuYHwZg1 10 | PMVgXPefnrgdYpvJQcgyTMM4xAcCAwEAAaNQME4wHQYDVR0OBBYEFJTymMh5e2/5 11 | cXZylzlA+SsYQd6qMB8GA1UdIwQYMBaAFJTymMh5e2/5cXZylzlA+SsYQd6qMAwG 12 | A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAMR1WpX/NiRRrpjAH7o5dNJv 13 | oYQPPXW5XTr/oHwFQpB+rDAus/GYJ0MmeGX4XkjgDKjENay+gMb1NX8FI3g7KWp3 14 | GqLsVZ9fYAc13bBVJeYC9Qr1wv6vBcTHslNDrOiCKEDlD67aTCoD+Q7HkSZPoJ9O 15 | 1htspTB6Yo2hLI6UVRVh1Ho/gKbK8xxG2/5um41dSKvgfzxo71DxIiQj7dJ3RmXC 16 | i+Z7MfWPHrtvKdb9W5hTgRVl1ooLClTsk/Urkabbi/NhfWG3p60LwXEfH++kMgTw 17 | geilBo7oqBopQ9KtH7yS2kG/YZdCZmYxF38v2NonqVW2VWRCf0WEkFCc6oj9Ep8= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /cert/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA3gg4uv4tPgAJI7x6i91Gh5nTJsuyH3vv03kXaCwDSBRMxeJo 3 | plv7/AckljEq/iUiCFMMFqp9ET/CjW4rb7Sna+XE4KKdtOnqybs1bdpfkt9pWwO8 4 | VGUvA1sfaV1UD6Vz+wuRURY4ri3BJ/p6dDesMSWDlDDkFYC6sSm/pE0YaMtlvxPW 5 | auIMeKGvp8Y0qbbLGM8RnDSIvig+DKqOMIimktqYyAFH/LqfHO18UGtySBxOhZ0v 6 | Zhv1j+I2PAEqaw1HHRoV5coHR9Y0XpLQjWlDbg1imNs5OKULlp0eDWZ+TLW0pHI9 7 | hdJK5gfBmDU8xWBc95+euB1im8lByDJMwzjEBwIDAQABAoIBAC6QKtWMWJCYEVKd 8 | RFXwocnuSInqTgCsTS9cRbxi/o0vNKqLQMcio0XHebTFuu5xeyGjyPU36+KiLHrc 9 | j99tU28dUJyLfedi5wpHaH0RPtneYInlzqkkTye1BHFkUSy21gvf8SnAL0LX9Zoc 10 | vXGB+2qTI3UAi9S2mnBtAPPD90qCeT2K5PIFKdNXaT6ssoZWMl7wu9Be5ardcOnR 11 | i5t5JgrNA6XsUcBYsjRHRKFkc9So+bFvCADg8MWc1FpiWqr7E9oZqLyaHlfSO2g3 12 | PCRSPyuuEYh94OthU++zQ1YUBJYPHu76BAvipFlrU9km48OcZ9GKI1LgvOjMxPtH 13 | ng8b5gECgYEA+/GKZ/iAOkJTYE1Jp4FohKOn6wi3QYbHJqQ+drAZk3U94HtU1uaj 14 | I5LfPBmC7ESjZb2x1Faizd6Lqg9NBcw1OHy6VSX703CPsvw1R9wiAN+67a8CPfpg 15 | KUAtEsksS7xGyip0PyloelrACZtwHC3NxYbeB75T3Jb3C8yEkUwnvDcCgYEA4Ztk 16 | bjTNL6HeUVOFeD1NhFmtW5pxPrCdqxdQkqPPTqVz64EgOpxN+Q/9GQMudgx48KI0 17 | yoWe+RRTYbimCr4HdtZcuEm8ale2NACQtl9aRK5DAvKASz0I/qFnpJLElCCbOmGv 18 | jsqOd4JYdXH9gr5P64VIgK4DpD1DzFVyQI4gbrECgYEAmKgscv8xwxs5JdruzB4v 19 | VF9Nea5YS838kb9nUcc2WBDsIz7XkzdAlmeB+AqFN+AHGbitLboo/5DfE3Yd92ta 20 | vBVnWfyf7+f+SjdjmV0bBWiK/UXTl/ZDmfsW1JEs098LDahXs4XFlo2yWp/ieFCQ 21 | pCUqF4sNiVW372BG2ztHwFUCgYAdx9BjB89KCVj6bUcCDz2LSfcrT2DBgLjfW82D 22 | /eGgpRB/NuVVN0rHpMepJJVeedkBErhbP3YztO4oySIJ6vm7QyKsJPedJgTkKmrv 23 | 1/hndoVb/zzWMbFdQ1sWC1Wto/w5oal19OEHf4pDvo9TUeHk2gwvCElsxbWMZH8X 24 | bGt1kQKBgQC+7z1M1cpRPyI3maGCeh3i39mq6H5Np8xEteKu4vG86ApgKvICIwu3 25 | LmqQbgJWYUhwMuEaeA8/SZZoD/wmTb4xPyq/Bylw3D96zEW9rcHg9ozg2nP0NcHT 26 | RlgqiRZRhfB9ulvlKDDirXvpRFApOm9U4+0W1ITBLu8B4TWPhdKyRg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /config/env/common.js: -------------------------------------------------------------------------------- 1 | export default { 2 | port: process.env.PORT || 3000 3 | } 4 | -------------------------------------------------------------------------------- /config/env/development.js: -------------------------------------------------------------------------------- 1 | export default { 2 | session: process.env.SESSION, 3 | token: process.env.JWT_SECRET, 4 | database: process.env.DATABASE_URL 5 | } 6 | -------------------------------------------------------------------------------- /config/env/production.js: -------------------------------------------------------------------------------- 1 | export default { 2 | session: process.env.SESSION, 3 | token: process.env.JWT_SECRET, 4 | database: process.env.DATABASE_URL 5 | } 6 | -------------------------------------------------------------------------------- /config/env/test.js: -------------------------------------------------------------------------------- 1 | export default { 2 | session: process.env.SESSION || 'testsession', 3 | token: process.env.JWT_SECRET || 'testsecret', 4 | database: process.env.DATABASE_URL || 'mongodb://localhost:27017/koach-test' 5 | } 6 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | import common from './env/common' 2 | 3 | const env = process.env.NODE_ENV || 'development' 4 | const config = require(`./env/${env}`).default 5 | 6 | export default Object.assign({}, common, config) 7 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | import passport from 'koa-passport' 2 | import User from '../src/models/users' 3 | import { Strategy } from 'passport-local' 4 | 5 | passport.serializeUser((user, done) => { 6 | done(null, user.id) 7 | }) 8 | 9 | passport.deserializeUser(async (id, done) => { 10 | try { 11 | const user = await User.findById(id, '-password') 12 | done(null, user) 13 | } catch (err) { 14 | done(err) 15 | } 16 | }) 17 | 18 | passport.use('local', new Strategy({ 19 | usernameField: 'username', 20 | passwordField: 'password' 21 | }, async (username, password, done) => { 22 | try { 23 | const user = await User.findOne({ username }) 24 | if (!user) { return done(null, false) } 25 | 26 | try { 27 | const isMatch = await user.validatePassword(password) 28 | 29 | if (!isMatch) { return done(null, false) } 30 | 31 | done(null, user) 32 | } catch (err) { 33 | done(err) 34 | } 35 | } catch (err) { 36 | return done(err) 37 | } 38 | })) 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | services: 3 | mongodb: 4 | image: mongo:latest 5 | restart: always 6 | container_name: "mongodb" 7 | environment: 8 | - MONGO_DATA_DIR=/db/data 9 | - MONGO_LOG_DIR=/dev/null 10 | volumes: 11 | - ${DB_PATH}:/data/db 12 | ports: 13 | - 27017:27017 14 | command: mongod --bind_ip_all --smallfiles --logpath=/dev/null # --quiet 15 | 16 | web: 17 | build: . 18 | links: 19 | - mongodb 20 | container_name: koach 21 | restart: always 22 | ports: 23 | - ${PORT}:${PORT} 24 | environment: 25 | - NODE_ENV=production 26 | - PORT=${PORT} 27 | - DATABASE_URL=${DATABASE_URL} 28 | - JWT_SECRET=${JWT_SECRET} 29 | - SESSION=${SESSION} 30 | depends_on: 31 | - mongodb -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register')() 2 | require('babel-polyfill') 3 | require('dotenv').config({ silent: process.env.NODE_ENV === 'production' }) 4 | require('./bin/server.js') 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koach", 3 | "version": "3.0.0", 4 | "description": "Production ready Koa2 boilerplate for APIs", 5 | "main": "index.js", 6 | "scripts": { 7 | "prod": "pm2 start pm2.config.js --env production", 8 | "prestart": "sh prestart.sh", 9 | "start": "./node_modules/.bin/nodemon index.js", 10 | "test": "NODE_ENV=test ./node_modules/.bin/mocha --require babel-register --require babel-polyfill", 11 | "lint": "eslint --ignore-path .eslintignore .", 12 | "fix": "eslint --fix --ignore-path .eslintignore .", 13 | "docs": "./node_modules/.bin/apidoc -i src/ -o docs" 14 | }, 15 | "keywords": [ 16 | "koa2-api-boilerplate", 17 | "api", 18 | "koa", 19 | "koa2", 20 | "boilerplate", 21 | "es6", 22 | "mongoose", 23 | "passportjs", 24 | "apidoc" 25 | ], 26 | "author": "Arpit Khandelwal ", 27 | "license": "MIT", 28 | "apidoc": { 29 | "title": "koach", 30 | "url": "localhost:3000" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/SystangoTechnologies/Koach.git" 35 | }, 36 | "dependencies": { 37 | "@hapi/joi": "^17.1.0", 38 | "apidoc": "^0.20.0", 39 | "babel-core": "^6.26.3", 40 | "babel-polyfill": "^6.26.0", 41 | "babel-preset-es2015-node5": "^1.2.0", 42 | "babel-preset-stage-0": "^6.24.1", 43 | "bcryptjs": "^2.4.3", 44 | "eslint-plugin-import": "^2.20.1", 45 | "eslint-plugin-node": "^11.0.0", 46 | "glob": "^7.1.6", 47 | "http2": "^3.3.7", 48 | "jsonwebtoken": "^8.5.1", 49 | "koa": "^2.11.0", 50 | "koa-bodyparser": "^4.2.1", 51 | "koa-convert": "^1.2.0", 52 | "koa-generic-session": "^2.0.4", 53 | "koa-helmet": "^5.2.0", 54 | "koa-logger": "^3.2.1", 55 | "koa-mount": "^4.0.0", 56 | "koa-passport": "^4.1.3", 57 | "koa-router": "^8.0.8", 58 | "koa-static": "^5.0.0", 59 | "mongoose": "^5.9.3", 60 | "passport-local": "^1.0.0", 61 | "pm2": "^4.2.3", 62 | "require-dir": "^1.2.0", 63 | "swagger-generator-koa": "^2.0.0" 64 | }, 65 | "devDependencies": { 66 | "babel-eslint": "^10.1.0", 67 | "babel-register": "^6.26.0", 68 | "chai": "^4.2.0", 69 | "dotenv": "^8.2.0", 70 | "eslint": "^6.8.0", 71 | "eslint-config-standard": "^14.1.0", 72 | "eslint-plugin-promise": "^4.2.1", 73 | "eslint-plugin-standard": "^4.0.1", 74 | "mocha": "^7.1.0", 75 | "nodemon": "^2.0.2", 76 | "supertest": "^4.0.2" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pm2.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [{ 3 | name: 'koach', 4 | script: './index.js', 5 | watch: true, 6 | instances: 1, 7 | exec_mode: 'cluster', 8 | autorestart: true, 9 | max_memory_restart: '500M', 10 | restart_delay: 3000 11 | }] 12 | } 13 | -------------------------------------------------------------------------------- /prestart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | eslint --ignore-path .eslintignore . -------------------------------------------------------------------------------- /src/middleware/index.js: -------------------------------------------------------------------------------- 1 | export function errorMiddleware () { 2 | return async (ctx, next) => { 3 | try { 4 | await next() 5 | } catch (err) { 6 | ctx.status = err.status || 500 7 | ctx.body = err.message 8 | ctx.app.emit('error', err, ctx) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/middleware/validators.js: -------------------------------------------------------------------------------- 1 | import User from '../models/users' 2 | import config from '../../config' 3 | import { getToken } from '../utils/auth' 4 | import { verify } from 'jsonwebtoken' 5 | 6 | export async function ensureUser (ctx, next) { 7 | const token = getToken(ctx) 8 | 9 | if (!token) { 10 | ctx.throw(401) 11 | } 12 | 13 | let decoded = null 14 | try { 15 | decoded = verify(token, config.token) 16 | } catch (err) { 17 | ctx.throw(401) 18 | } 19 | 20 | ctx.state.user = await User.findById(decoded.id, '-password') 21 | if (!ctx.state.user) { 22 | ctx.throw(401) 23 | } 24 | 25 | return next() 26 | } 27 | -------------------------------------------------------------------------------- /src/models/users.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import bcrypt from 'bcryptjs' 3 | import config from '../../config' 4 | import jwt from 'jsonwebtoken' 5 | 6 | const User = new mongoose.Schema({ 7 | type: { 8 | type: String, 9 | default: 'User' 10 | }, 11 | name: { 12 | type: String 13 | }, 14 | username: { 15 | type: String, 16 | required: true, 17 | unique: true 18 | }, 19 | password: { 20 | type: String, 21 | required: true 22 | } 23 | }) 24 | 25 | User.pre('save', function preSave (next) { 26 | try { 27 | const user = this 28 | if (!user.isModified('password')) { 29 | return next() 30 | } 31 | let salt = bcrypt.genSaltSync(10) 32 | var hash = bcrypt.hashSync(user.password, salt) 33 | user.password = hash 34 | next(null) 35 | } catch (error) { 36 | next(error) 37 | } 38 | }) 39 | 40 | User.methods.validatePassword = function validatePassword (password) { 41 | const user = this 42 | return new Promise((resolve, reject) => { 43 | try { 44 | let isMatch = bcrypt.compareSync(password, user.password) 45 | resolve(isMatch) 46 | } catch (error) { 47 | resolve(false) 48 | } 49 | }) 50 | } 51 | 52 | User.methods.generateToken = function generateToken () { 53 | const user = this 54 | 55 | return jwt.sign({ 56 | id: user.id 57 | }, config.token) 58 | } 59 | 60 | export default mongoose.model('user', User) 61 | -------------------------------------------------------------------------------- /src/modules/common/home/controller.js: -------------------------------------------------------------------------------- 1 | export async function getHome (ctx) { 2 | ctx.body = 'The KOACH is here!' 3 | } 4 | 5 | export async function getLogin (ctx) { 6 | ctx.body = 'The KOACH login page is WIP.' 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/common/home/router.js: -------------------------------------------------------------------------------- 1 | import * as home from './controller' 2 | 3 | export default [ 4 | { 5 | method: 'GET', 6 | route: '/', 7 | handlers: [ 8 | home.getHome 9 | ] 10 | }, 11 | { 12 | method: 'GET', 13 | route: '/login', 14 | handlers: [ 15 | home.getLogin 16 | ] 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /src/modules/common/index.js: -------------------------------------------------------------------------------- 1 | import glob from 'glob' 2 | import Router from 'koa-router' 3 | 4 | exports = module.exports = function initModules (app) { 5 | return new Promise((resolve, reject) => { 6 | try { 7 | glob(`${__dirname}/*`, { ignore: '**/index.js' }, (err, matches) => { 8 | if (err) { reject(err) } 9 | matches.forEach((mod) => { 10 | console.log(`${mod}/router`) 11 | const router = require(`${mod}/router`) 12 | const routes = router.default 13 | const baseUrl = router.baseUrl 14 | const instance = new Router({ prefix: baseUrl }) 15 | 16 | routes.forEach((config) => { 17 | const { 18 | method = '', 19 | route = '', 20 | handlers = [] 21 | } = config 22 | const lastHandler = handlers.pop() 23 | instance[method.toLowerCase()](route, ...handlers, async function (ctx) { 24 | await lastHandler(ctx) 25 | }) 26 | app 27 | .use(instance.routes()) 28 | .use(instance.allowedMethods()) 29 | }) 30 | }) 31 | resolve(); 32 | }) 33 | } catch (error) { 34 | reject(error); 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/v1/auth/controller.js: -------------------------------------------------------------------------------- 1 | import passport from 'koa-passport' 2 | import constants from './../../../utils/constants' 3 | 4 | /** 5 | * @apiDefine TokenError 6 | * @apiError Unauthorized Invalid JWT token 7 | * 8 | * @apiErrorExample {json} Unauthorized-Error: 9 | * HTTP/1.1 401 Unauthorized 10 | * { 11 | * "status": 401, 12 | * "error": "Unauthorized" 13 | * } 14 | */ 15 | 16 | /** 17 | * @api {post} /v1/auth Authenticate user 18 | * @apiVersion 1.0.0 19 | * @apiName AuthUser 20 | * @apiGroup Auth 21 | * 22 | * @apiParam {String} username User username. 23 | * @apiParam {String} password User password. 24 | * 25 | * @apiExample Example usage: 26 | * curl -H "Content-Type: application/json" -X POST -d '{ "username": "johndoe, "password": "foo" }' localhost:3000/v1/auth 27 | * 28 | * @apiSuccess {Object} user User object 29 | * @apiSuccess {ObjectId} user._id User id 30 | * @apiSuccess {String} user.name User name 31 | * @apiSuccess {String} user.username User username 32 | * @apiSuccess {String} token Encoded JWT 33 | * 34 | * @apiSuccessExample {json} Success-Response: 35 | * HTTP/1.1 200 OK 36 | * { 37 | * "user": { 38 | * "_id": "56bd1da600a526986cf65c80" 39 | * "username": "johndoe" 40 | * }, 41 | * "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" 42 | * } 43 | * 44 | * @apiError Unauthorized Incorrect credentials 45 | * 46 | * @apiErrorExample {json} Error-Response: 47 | * HTTP/1.1 401 Unauthorized 48 | * { 49 | * "status": 401, 50 | * "error": "Unauthorized" 51 | * } 52 | */ 53 | 54 | export async function authUser(ctx, next) { 55 | try { 56 | return passport.authenticate('local', (err, user) => { 57 | if (err || !user) { 58 | ctx.throw(401) 59 | } 60 | const token = user.generateToken() 61 | const response = user.toJSON() 62 | delete response.password 63 | delete response.__v 64 | ctx.status = constants.STATUS_CODE.SUCCESS_STATUS 65 | ctx.body = { 66 | user: response 67 | } 68 | ctx.append('Authorization', token); 69 | })(ctx, next) 70 | } catch (error) { 71 | ctx.body = error; 72 | ctx.status = constants.STATUS_CODE.INTERNAL_SERVER_ERROR_STATUS 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/modules/v1/auth/router.js: -------------------------------------------------------------------------------- 1 | import * as auth from './controller' 2 | import requestModel from '../../../requestModel/v1/auth'; 3 | import {validation} from 'swagger-generator-koa' 4 | 5 | export const baseUrl = '/auth' 6 | 7 | export default [ 8 | { 9 | method: 'POST', 10 | route: '/', 11 | handlers: [ 12 | validation(requestModel[0]), 13 | auth.authUser 14 | ] 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /src/modules/v1/index.js: -------------------------------------------------------------------------------- 1 | import glob from 'glob' 2 | import Router from 'koa-router' 3 | exports = module.exports = function initModules (app) { 4 | return new Promise((resolve, reject) => { 5 | try { 6 | glob(`${__dirname}/*`, { ignore: '**/index.js' }, (err, matches) => { 7 | if (err) { reject(err) } 8 | matches.forEach((mod) => { 9 | const router = require(`${mod}/router`) 10 | 11 | const routes = router.default 12 | const baseUrl = router.baseUrl 13 | const instance = new Router({ prefix: '/v1' + baseUrl }) 14 | 15 | routes.forEach((config) => { 16 | const { 17 | method = '', 18 | route = '', 19 | handlers = [] 20 | } = config 21 | 22 | const lastHandler = handlers.pop() 23 | 24 | instance[method.toLowerCase()](route, ...handlers, async function (ctx) { 25 | await lastHandler(ctx) 26 | }) 27 | app 28 | .use(instance.routes()) 29 | .use(instance.allowedMethods()) 30 | }) 31 | }) 32 | resolve(); 33 | }) 34 | } catch (error) { 35 | reject(error); 36 | } 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/modules/v1/users/controller.js: -------------------------------------------------------------------------------- 1 | import User from '../../../models/users' 2 | import constants from './../../../utils/constants' 3 | import mongoose from 'mongoose' 4 | /** 5 | * @api {post} /v1/users Create a new user 6 | * @apiPermission 7 | * @apiVersion 1.0.0 8 | * @apiName CreateUser 9 | * @apiGroup Users 10 | * 11 | * @apiExample Example usage: 12 | * curl -H "Content-Type: application/json" -X POST -d '{ "user": { "username": "johndoe", "password": "secretpasas" } }' localhost:3000/v1/users 13 | * 14 | * @apiParam {Object} user User object (required) 15 | * @apiParam {String} user.username Username. 16 | * @apiParam {String} user.password Password. 17 | * 18 | * @apiSuccess {Object} users User object 19 | * @apiSuccess {ObjectId} users._id User id 20 | * @apiSuccess {String} users.name User name 21 | * @apiSuccess {String} users.username User username 22 | * 23 | * @apiSuccessExample {json} Success-Response: 24 | * HTTP/1.1 200 OK 25 | * { 26 | * "user": { 27 | * "_id": "56bd1da600a526986cf65c80" 28 | * "name": "John Doe" 29 | * "username": "johndoe" 30 | * } 31 | * } 32 | * 33 | * @apiError UnprocessableEntity Missing required parameters 34 | * 35 | * @apiErrorExample {json} Error-Response: 36 | * HTTP/1.1 422 Unprocessable Entity 37 | * { 38 | * "status": 422, 39 | * "error": "Unprocessable Entity" 40 | * } 41 | */ 42 | export async function createUser(ctx) { 43 | const user = new User(ctx.request.body) 44 | try { 45 | let userData = await User.findOne({ 46 | username: ctx.request.body.username 47 | }); 48 | if(userData) { 49 | ctx.body = constants.MESSAGES.USER_ALREADY_EXIST; 50 | ctx.status = constants.STATUS_CODE.CONFLICT_ERROR_STATUS 51 | return; 52 | } 53 | await user.save() 54 | const token = user.generateToken() 55 | const response = user.toJSON() 56 | delete response.password 57 | ctx.body = { 58 | user: response 59 | } 60 | ctx.append('Authorization', token); 61 | ctx.status = constants.STATUS_CODE.CREATED_SUCCESSFULLY_STATUS; 62 | } catch (error) { 63 | console.log('Error while creating user', error); 64 | ctx.body = error; 65 | ctx.status = constants.STATUS_CODE.INTERNAL_SERVER_ERROR_STATUS 66 | } 67 | } 68 | 69 | /** 70 | * @api {get} /v1/users Get all users 71 | * @apiPermission user 72 | * @apiVersion 1.0.0 73 | * @apiName GetUsers 74 | * @apiGroup Users 75 | * 76 | * @apiExample Example usage: 77 | * curl -H "Content-Type: application/json" -X GET localhost:3000/v1/users 78 | * 79 | * @apiSuccess {Object[]} users Array of user objects 80 | * @apiSuccess {ObjectId} users._id User id 81 | * @apiSuccess {String} users.name User name 82 | * @apiSuccess {String} users.username User username 83 | * 84 | * @apiSuccessExample {json} Success-Response: 85 | * HTTP/1.1 200 OK 86 | * { 87 | * "users": [{ 88 | * "_id": "56bd1da600a526986cf65c80" 89 | * "name": "John Doe" 90 | * "username": "johndoe" 91 | * }] 92 | * } 93 | * 94 | * @apiUse TokenError 95 | */ 96 | export async function getUsers(ctx) { 97 | try { 98 | const users = await User.find({}, '-password -__v') 99 | ctx.body = users; 100 | ctx.status = constants.STATUS_CODE.SUCCESS_STATUS; 101 | } catch (error) { 102 | ctx.body = error; 103 | ctx.status = constants.STATUS_CODE.INTERNAL_SERVER_ERROR_STATUS 104 | } 105 | } 106 | 107 | /** 108 | * @api {get} /v1/users/:id Get user by id 109 | * @apiPermission user 110 | * @apiVersion 1.0.0 111 | * @apiName GetUser 112 | * @apiGroup Users 113 | * 114 | * @apiExample Example usage: 115 | * curl -H "Content-Type: application/json" -X GET localhost:3000/v1/users/56bd1da600a526986cf65c80 116 | * 117 | * @apiSuccess {Object} users User object 118 | * @apiSuccess {ObjectId} users._id User id 119 | * @apiSuccess {String} users.name User name 120 | * @apiSuccess {String} users.username User username 121 | * 122 | * @apiSuccessExample {json} Success-Response: 123 | * HTTP/1.1 200 OK 124 | * { 125 | * "user": { 126 | * "_id": "56bd1da600a526986cf65c80" 127 | * "name": "John Doe" 128 | * "username": "johndoe" 129 | * } 130 | * } 131 | * 132 | * @apiUse TokenError 133 | */ 134 | export async function getUser(ctx, next) { 135 | try { 136 | const user = await User.findById(ctx.params.id, '-password -__v') 137 | if (!user) { 138 | ctx.status = constants.STATUS_CODE.NO_CONTENT_STATUS 139 | ctx.body = { 140 | message: constants.MESSAGES.USER_NOT_FOUND 141 | } 142 | return 143 | } 144 | ctx.body = user 145 | ctx.status = constants.STATUS_CODE.SUCCESS_STATUS; 146 | } catch (error) { 147 | ctx.body = error; 148 | ctx.status = constants.STATUS_CODE.INTERNAL_SERVER_ERROR_STATUS 149 | } 150 | } 151 | 152 | /** 153 | * @api {put} /v1/users/:id Update a user 154 | * @apiPermission 155 | * @apiVersion 1.0.0 156 | * @apiName UpdateUser 157 | * @apiGroup Users 158 | * 159 | * @apiExample Example usage: 160 | * curl -H "Content-Type: application/json" -X PUT -d '{ "user": { "name": "Cool new Name" } }' localhost:3000/v1/users/56bd1da600a526986cf65c80 161 | * 162 | * @apiParam {Object} user User object (required) 163 | * @apiParam {String} user.name Name. 164 | * @apiParam {String} user.username Username. 165 | * 166 | * @apiSuccess {Object} users User object 167 | * @apiSuccess {ObjectId} users._id User id 168 | * @apiSuccess {String} users.name Updated name 169 | * @apiSuccess {String} users.username Updated username 170 | * 171 | * @apiSuccessExample {json} Success-Response: 172 | * HTTP/1.1 201 CREATED 173 | * 174 | * @apiError UnprocessableEntity Missing required parameters 175 | * 176 | * @apiErrorExample {json} Error-Response: 177 | * HTTP/1.1 422 Unprocessable Entity 178 | * { 179 | * "status": 422, 180 | * "error": "Unprocessable Entity" 181 | * } 182 | * 183 | * @apiUse TokenError 184 | */ 185 | export async function updateUser(ctx) { 186 | try { 187 | const user = ctx.request.body; 188 | await User.findOneAndUpdate({ 189 | _id: mongoose.Types.ObjectId(ctx.params.id) 190 | }, { 191 | $set: { 192 | name: user.name 193 | } 194 | }) 195 | ctx.status = constants.STATUS_CODE.CREATED_SUCCESSFULLY_STATUS; 196 | } catch (error) { 197 | console.log('Error', error) 198 | ctx.body = error; 199 | ctx.status = constants.STATUS_CODE.INTERNAL_SERVER_ERROR_STATUS 200 | } 201 | } 202 | 203 | /** 204 | * @api {delete} /v1/users/:id Delete a user 205 | * @apiPermission 206 | * @apiVersion 1.0.0 207 | * @apiName DeleteUser 208 | * @apiGroup Users 209 | * 210 | * @apiExample Example usage: 211 | * curl -H "Content-Type: application/json" -X DELETE localhost:3000/v1/users/56bd1da600a526986cf65c80 212 | * 213 | * @apiSuccess {StatusCode} 200 214 | * 215 | * @apiSuccessExample {json} Success-Response: 216 | * HTTP/1.1 200 OK 217 | * { 218 | * "success": true 219 | * } 220 | * 221 | * @apiError UnprocessableEntity Missing required parameters 222 | * 223 | * @apiErrorExample {json} Error-Response: 224 | * HTTP/1.1 400 Bad request 225 | * { 226 | * "status": 400, 227 | * "error": "Bad request" 228 | * } 229 | * @apiUse TokenError 230 | */ 231 | 232 | export async function deleteUser(ctx) { 233 | try { 234 | await User.findByIdAndRemove(mongoose.Types.ObjectId(ctx.params.id)); 235 | ctx.status = constants.STATUS_CODE.SUCCESS_STATUS; 236 | ctx.body = { 237 | success: true 238 | } 239 | } catch (error) { 240 | ctx.body = error; 241 | ctx.status = constants.STATUS_CODE.INTERNAL_SERVER_ERROR_STATUS 242 | } 243 | } 244 | 245 | /** 246 | * @api {patch} /v1/users/_change-password Upadete password of a user 247 | * @apiPermission 248 | * @apiVersion 1.0.0 249 | * @apiName changePassword 250 | * @apiGroup Users 251 | * 252 | * @apiExample Example usage: 253 | * curl -H "Content-Type: application/json" -X PATCH -d '{ "oldPassword":"oldPassword", "newPassword": "newPassword" }' localhost:3000/v1/users/_changePassword 254 | * 255 | * @apiSuccess {StatusCode} 200 256 | * 257 | * @apiSuccessExample {json} Success-Response: 258 | * HTTP/1.1 200 OK 259 | * { 260 | * "success": true 261 | * } 262 | * 263 | * @apiUse TokenError 264 | */ 265 | 266 | export async function changePassword(ctx) { 267 | try { 268 | let oldPassword = ctx.request.body.oldPassword 269 | let newPassword = ctx.request.body.newPassword 270 | 271 | if(!newPassword || !oldPassword) { 272 | ctx.status = constants.STATUS_CODE.BAD_REQUEST_ERROR_STATUS 273 | return 274 | } 275 | 276 | let user = await User.findOne({ 277 | _id: mongoose.Types.ObjectId(ctx.state.user.id) 278 | }) 279 | 280 | let isMatch = await user.validatePassword(oldPassword) 281 | 282 | if(isMatch) { 283 | user.password = newPassword 284 | user.save(); 285 | ctx.status = constants.STATUS_CODE.SUCCESS_STATUS; 286 | ctx.body = { 287 | success: true 288 | } 289 | return 290 | } else { 291 | ctx.body = { 292 | success: false 293 | } 294 | ctx.status = constants.STATUS_CODE.UNAUTHORIZED_ERROR_STATUS 295 | return 296 | } 297 | } catch (error) { 298 | ctx.body = error; 299 | ctx.status = constants.STATUS_CODE.INTERNAL_SERVER_ERROR_STATUS 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/modules/v1/users/router.js: -------------------------------------------------------------------------------- 1 | 'use-strict' 2 | import * as user from './controller' 3 | import { validation } from 'swagger-generator-koa' 4 | 5 | import { ensureUser } from '../../../middleware/validators' 6 | import requestModel from '../../../requestModel/v1/users'; 7 | export const baseUrl = '/users' 8 | 9 | export default [ 10 | { 11 | method: 'POST', 12 | route: '/', 13 | handlers: [ 14 | validation(requestModel[0]), 15 | user.createUser 16 | ] 17 | }, 18 | { 19 | method: 'GET', 20 | route: '/', 21 | handlers: [ 22 | ensureUser, 23 | user.getUsers 24 | ] 25 | }, 26 | { 27 | method: 'GET', 28 | route: '/:id', 29 | handlers: [ 30 | ensureUser, 31 | validation(requestModel[2]), 32 | user.getUser 33 | ] 34 | }, 35 | { 36 | method: 'PUT', 37 | route: '/:id', 38 | handlers: [ 39 | ensureUser, 40 | validation(requestModel[3]), 41 | user.updateUser 42 | ] 43 | }, 44 | { 45 | method: 'DELETE', 46 | route: '/:id', 47 | handlers: [ 48 | ensureUser, 49 | validation(requestModel[4]), 50 | user.deleteUser 51 | ] 52 | }, 53 | { 54 | method: 'PATCH', 55 | route: '/_change-password', 56 | handlers: [ 57 | ensureUser, 58 | user.changePassword 59 | ] 60 | } 61 | ] 62 | -------------------------------------------------------------------------------- /src/modules/v2/auth/controller.js: -------------------------------------------------------------------------------- 1 | import passport from 'koa-passport' 2 | import constants from './../../../utils/constants' 3 | 4 | /** 5 | * @apiDefine TokenError 6 | * @apiError Unauthorized Invalid JWT token 7 | * 8 | * @apiErrorExample {json} Unauthorized-Error: 9 | * HTTP/1.1 401 Unauthorized 10 | * { 11 | * "status": 401, 12 | * "error": "Unauthorized" 13 | * } 14 | */ 15 | 16 | /** 17 | * @api {post} /v2/auth Authenticate user 18 | * @apiVersion 1.0.0 19 | * @apiName AuthUser 20 | * @apiGroup Auth 21 | * 22 | * @apiParam {String} username User username. 23 | * @apiParam {String} password User password. 24 | * 25 | * @apiExample Example usage: 26 | * curl -H "Content-Type: application/json" -X POST -d '{ "username": "johndoe, "password": "foo" }' localhost:3000/v2/auth 27 | * 28 | * @apiSuccess {Object} user User object 29 | * @apiSuccess {ObjectId} user._id User id 30 | * @apiSuccess {String} user.name User name 31 | * @apiSuccess {String} user.username User username 32 | * @apiSuccess {String} token Encoded JWT 33 | * 34 | * @apiSuccessExample {json} Success-Response: 35 | * HTTP/1.1 200 OK 36 | * { 37 | * "user": { 38 | * "_id": "56bd1da600a526986cf65c80" 39 | * "username": "johndoe" 40 | * }, 41 | * "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" 42 | * } 43 | * 44 | * @apiError Unauthorized Incorrect credentials 45 | * 46 | * @apiErrorExample {json} Error-Response: 47 | * HTTP/1.1 401 Unauthorized 48 | * { 49 | * "status": 401, 50 | * "error": "Unauthorized" 51 | * } 52 | */ 53 | 54 | export async function authUser (ctx, next) { 55 | try { 56 | return passport.authenticate('local', (err, user) => { 57 | if (err || !user) { 58 | ctx.throw(401) 59 | } 60 | 61 | const token = user.generateToken() 62 | 63 | const response = user.toJSON() 64 | 65 | delete response.password 66 | delete response.__v 67 | ctx.status = constants.STATUS_CODE.SUCCESS_STATUS 68 | ctx.body = { 69 | user: response 70 | } 71 | ctx.append('Authorization', token); 72 | })(ctx, next) 73 | } catch (error) { 74 | ctx.body = error; 75 | ctx.status = constants.STATUS_CODE.INTERNAL_SERVER_ERROR_STATUS 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/modules/v2/auth/router.js: -------------------------------------------------------------------------------- 1 | import * as auth from './controller' 2 | import requestModel from '../../../requestModel/v2/auth'; 3 | import {validation} from 'swagger-generator-koa' 4 | 5 | export const baseUrl = '/auth' 6 | 7 | export default [ 8 | { 9 | method: 'POST', 10 | route: '/', 11 | handlers: [ 12 | validation(requestModel[0]), 13 | auth.authUser 14 | ] 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /src/modules/v2/index.js: -------------------------------------------------------------------------------- 1 | import glob from 'glob' 2 | import Router from 'koa-router' 3 | 4 | exports = module.exports = function initModules(app) { 5 | return new Promise((resolve, reject) => { 6 | try { 7 | glob(`${__dirname}/*`, { ignore: '**/index.js' }, (err, matches) => { 8 | if (err) { reject(err) } 9 | matches.forEach((mod) => { 10 | const router = require(`${mod}/router`) 11 | 12 | const routes = router.default 13 | const baseUrl = router.baseUrl 14 | const instance = new Router({ prefix: '/v2' + baseUrl }) 15 | 16 | routes.forEach((config) => { 17 | const { 18 | method = '', 19 | route = '', 20 | handlers = [] 21 | } = config 22 | 23 | const lastHandler = handlers.pop() 24 | 25 | instance[method.toLowerCase()](route, ...handlers, async function (ctx) { 26 | await lastHandler(ctx) 27 | }) 28 | 29 | app 30 | .use(instance.routes()) 31 | .use(instance.allowedMethods()) 32 | }) 33 | }) 34 | resolve(); 35 | }) 36 | } catch (error) { 37 | reject(); 38 | } 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/v2/users/controller.js: -------------------------------------------------------------------------------- 1 | import User from '../../../models/users' 2 | import constants from './../../../utils/constants' 3 | 4 | /** 5 | * @api {get} /v2//users Get all users 6 | * @apiPermission user 7 | * @apiVersion 1.0.0 8 | * @apiName GetUsers 9 | * @apiGroup Users 10 | * 11 | * @apiExample Example usage: 12 | * curl -H "Content-Type: application/json" -X GET localhost:3000/v2/users 13 | * 14 | * @apiSuccess {Object[]} users Array of user objects 15 | * @apiSuccess {ObjectId} users._id User id 16 | * @apiSuccess {String} users.name User name 17 | * @apiSuccess {String} users.username User username 18 | * 19 | * @apiSuccessExample {json} Success-Response: 20 | * HTTP/1.1 200 OK 21 | * { 22 | * "users": [{ 23 | * "_id": "56bd1da600a526986cf65c80" 24 | * "name": "John Doe" 25 | * "username": "johndoe" 26 | * }] 27 | * } 28 | * 29 | * @apiUse TokenError 30 | */ 31 | export async function getUsers (ctx) { 32 | try { 33 | const users = await User.find({}, '-password -__v') 34 | ctx.body = users 35 | ctx.status = constants.STATUS_CODE.SUCCESS_STATUS; 36 | } catch (error) { 37 | ctx.body = error; 38 | ctx.status = constants.STATUS_CODE.INTERNAL_SERVER_ERROR_STATUS 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/v2/users/router.js: -------------------------------------------------------------------------------- 1 | import { ensureUser } from '../../../middleware/validators' 2 | import * as user from './controller' 3 | 4 | export const baseUrl = '/users' 5 | 6 | export default [ 7 | { 8 | method: 'GET', 9 | route: '/', 10 | handlers: [ 11 | ensureUser, 12 | user.getUsers 13 | ] 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /src/requestModel/v1/auth.js: -------------------------------------------------------------------------------- 1 | import Joi from '@hapi/joi' 2 | const GROUP= `Auth V1`; 3 | /** 4 | * File name for request and response model should be same as router file. 5 | * Define request model with there order in router js. 6 | * For example first api in user router is is create user so we define schema with key 0. 7 | */ 8 | module.exports = { 9 | // Here 0 is the order of api route file. 10 | 0: { 11 | body: { 12 | username: Joi.string().required(), 13 | password: Joi.string().required() 14 | }, 15 | model: 'Login', 16 | group: GROUP, 17 | description: 'Login user into the system' 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/requestModel/v1/users.js: -------------------------------------------------------------------------------- 1 | const Joi = require('@hapi/joi'); 2 | const GROUP = `Users V1`; 3 | /** 4 | * File name for request and response model should be same as router file. 5 | * Define request model with there order in router js. 6 | * For example first api in user router is is create user so we define schema with key 0. 7 | */ 8 | module.exports = { 9 | // Here 0 is the order of api route file. 10 | 0: { 11 | 12 | body: { 13 | type: Joi.string(), 14 | name: Joi.string().required(), 15 | username: Joi.string().required(), 16 | password: Joi.string().required() 17 | }, 18 | model: 'CreateUser', 19 | group: GROUP, 20 | description: 'Create user and save in database' 21 | }, 22 | 1: { 23 | group: GROUP, 24 | model: 'GetUsers', 25 | description: 'Get users from database' 26 | }, 27 | 2: { 28 | path: { 29 | id: Joi.string().required() 30 | }, 31 | group: GROUP, 32 | model: 'GetUserDetails', 33 | description: 'Get user details from database' 34 | }, 35 | 3: { 36 | path: { 37 | id: Joi.string().required() 38 | }, 39 | body: { 40 | type: Joi.string(), 41 | name: Joi.string().required(), 42 | username: Joi.string().required() 43 | }, 44 | model: 'UpdateUser', 45 | group: GROUP, 46 | description: 'Update user information' 47 | }, 48 | 4: { 49 | path: { 50 | id: Joi.string().required() 51 | }, 52 | group: GROUP, 53 | model: 'DeleteUser', 54 | description: 'Delete user from database' 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/requestModel/v2/auth.js: -------------------------------------------------------------------------------- 1 | import Joi from '@hapi/joi' 2 | const GROUP= `Auth V2`; 3 | /** 4 | * File name for request and response model should be same as router file. 5 | * Define request model with there order in router js. 6 | * For example first api in user router is is create user so we define schema with key 0. 7 | */ 8 | module.exports = { 9 | // Here 0 is the order of api route file. 10 | 0: { 11 | body: { 12 | username: Joi.string().required(), 13 | password: Joi.string().required() 14 | }, 15 | model: 'Login', 16 | group: GROUP, 17 | description: 'Login user into the system' 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/requestModel/v2/users.js: -------------------------------------------------------------------------------- 1 | const GROUP = `Users V2`; 2 | /** 3 | * File name for request and response model should be same as router file. 4 | * Define request model with there order in router js. 5 | * For example first api in user router is is create user so we define schema with key 0. 6 | */ 7 | module.exports = { 8 | // Here 0 is the order of api route file. 9 | 0: { 10 | group: GROUP, 11 | model: 'GetUsers', 12 | description: 'Get users from database' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/responseModel/v1/auth.js: -------------------------------------------------------------------------------- 1 | // The name of each response payload should be model name defined in Request model schema. 2 | 3 | module.exports = { 4 | Login: { // This name should be model name defined in request model. 5 | 201: { 6 | _id: 'number', 7 | type: 'string', 8 | name: 'string', 9 | username: 'string' 10 | }, 11 | 401: { 12 | message: 'string', 13 | error: 'string' 14 | }, 15 | 400: { 16 | message: 'string', 17 | error: 'string' 18 | }, 19 | 500: { 20 | message: 'string', 21 | error: 'string' 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/responseModel/v1/users.js: -------------------------------------------------------------------------------- 1 | // The name of each response payload should be model name defined in Request model schema. 2 | 3 | module.exports = { 4 | CreateUser: { // This name should be model name defined in request model. 5 | 201: { 6 | message: 'string' 7 | }, 8 | 401: { 9 | message: 'string', 10 | error: 'string' 11 | }, 12 | 400: { 13 | message: 'string', 14 | error: 'string' 15 | }, 16 | 500: { 17 | message: 'string', 18 | error: 'string' 19 | } 20 | }, 21 | GetUsers: { 22 | 200: [{ 23 | _id: 'number', 24 | type: 'string', 25 | name: 'string', 26 | username: 'string' 27 | }], 28 | 401: { 29 | message: 'string', 30 | error: 'string' 31 | }, 32 | 400: { 33 | message: 'string', 34 | error: 'string' 35 | }, 36 | 500: { 37 | message: 'string', 38 | error: 'string' 39 | } 40 | }, 41 | UpdateUser: { 42 | 201: { 43 | _id: 'number', 44 | type: 'string', 45 | name: 'string', 46 | username: 'string' 47 | }, 48 | 401: { 49 | message: 'string', 50 | error: 'string' 51 | }, 52 | 400: { 53 | message: 'string', 54 | error: 'string' 55 | }, 56 | 500: { 57 | message: 'string', 58 | error: 'string' 59 | } 60 | }, 61 | GetUserDetails: { 62 | 200: { 63 | _id: 'number', 64 | type: 'string', 65 | name: 'string', 66 | username: 'string' 67 | }, 68 | 401: { 69 | message: 'string', 70 | error: 'string' 71 | }, 72 | 400: { 73 | message: 'string', 74 | error: 'string' 75 | }, 76 | 500: { 77 | message: 'string', 78 | error: 'string' 79 | } 80 | }, 81 | DeleteUser: { 82 | 204: { 83 | }, 84 | 401: { 85 | message: 'string', 86 | error: 'string' 87 | }, 88 | 400: { 89 | message: 'string', 90 | error: 'string' 91 | }, 92 | 500: { 93 | message: 'string', 94 | error: 'string' 95 | } 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /src/responseModel/v2/auth.js: -------------------------------------------------------------------------------- 1 | // The name of each response payload should be model name defined in Request model schema. 2 | 3 | module.exports = { 4 | Login: { // This name should be model name defined in request model. 5 | 201: { 6 | _id: 'number', 7 | type: 'string', 8 | name: 'string', 9 | username: 'string' 10 | }, 11 | 401: { 12 | message: 'string', 13 | error: 'string' 14 | }, 15 | 400: { 16 | message: 'string', 17 | error: 'string' 18 | }, 19 | 500: { 20 | message: 'string', 21 | error: 'string' 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/responseModel/v2/users.js: -------------------------------------------------------------------------------- 1 | // The name of each response payload should be model name defined in Request model schema. 2 | 3 | module.exports = { 4 | GetUsers: { 5 | 200: [{ 6 | _id: 'number', 7 | type: 'string', 8 | name: 'string', 9 | username: 'string' 10 | }], 11 | 401: { 12 | message: 'string', 13 | error: 'string' 14 | }, 15 | 400: { 16 | message: 'string', 17 | error: 'string' 18 | }, 19 | 500: { 20 | message: 'string', 21 | error: 'string' 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | export function getToken (ctx) { 2 | const header = ctx.request.header.authorization 3 | if (!header) { 4 | return null 5 | } 6 | const parts = header.split(' ') 7 | if (parts.length !== 2) { 8 | return null 9 | } 10 | const scheme = parts[0] 11 | const token = parts[1] 12 | if (/^Bearer$/i.test(scheme)) { 13 | return token 14 | } 15 | return null 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | STATUS_CODE: { 3 | SUCCESS_STATUS: 200, 4 | NO_CONTENT_STATUS: 204, 5 | ACCEPTED_STATUS: 202, 6 | UNPROCESSABLE_ENTITY_STATUS: 422, 7 | INTERNAL_SERVER_ERROR_STATUS: 500, 8 | BAD_REQUEST_ERROR_STATUS: 400, 9 | UNAUTHORIZED_ERROR_STATUS: 401, 10 | FORBIDDEN_ERROR_STATUS: 403, 11 | CONFLICT_ERROR_STATUS: 409, 12 | MOVED_PERMANENTLY: 301, 13 | NOT_FOUND_STATUS: 404, 14 | CREATED_SUCCESSFULLY_STATUS: 201 15 | }, 16 | MESSAGES: { 17 | USER_NOT_FOUND: 'User not found' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /static/Average.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/koach-javascript/36830c836548d9dd7585a755173108b41a2651f9/static/Average.png -------------------------------------------------------------------------------- /static/Throughput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/koach-javascript/36830c836548d9dd7585a755173108b41a2651f9/static/Throughput.png -------------------------------------------------------------------------------- /static/koach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SystangoTechnologies/koach-javascript/36830c836548d9dd7585a755173108b41a2651f9/static/koach.png -------------------------------------------------------------------------------- /test/auth.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* eslint-disable no-undef */ 3 | import app from '../bin/server' 4 | import supertest from 'supertest' 5 | import { should } from 'chai' 6 | import { cleanDb, authUser } from './utils' 7 | 8 | should() 9 | const request = supertest.agent(app.listen()) 10 | const context = {} 11 | 12 | describe('Auth', () => { 13 | before((done) => { 14 | cleanDb() 15 | authUser(request, (err, { user, token }) => { 16 | if (err) { return done(err) } 17 | 18 | context.user = user 19 | context.token = token 20 | done() 21 | }) 22 | }) 23 | 24 | }) 25 | -------------------------------------------------------------------------------- /test/users.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* eslint-disable no-undef */ 3 | import app from '../bin/server' 4 | import supertest from 'supertest' 5 | import { expect, should } from 'chai' 6 | import { cleanDb } from './utils' 7 | 8 | should() 9 | const request = supertest.agent(app.listen()) 10 | const context = {} 11 | 12 | // eslint-disable-next-line no-undef 13 | describe('Users', () => { 14 | before((done) => { 15 | cleanDb() 16 | done() 17 | }) 18 | 19 | describe('POST /v1/users', () => { 20 | it('should reject signup when data is incomplete', (done) => { 21 | request 22 | .post('/v1/users') 23 | .set('Accept', 'application/json') 24 | .send({ username: 'supercoolname' }) 25 | .expect(500, done) 26 | }) 27 | 28 | it('should sign up', (done) => { 29 | request 30 | .post('/v1/users') 31 | .set('Accept', 'application/json') 32 | .send({ user: { username: 'supercoolname', name: 'test', password: 'supersecretpassword' } }) 33 | .expect(201, (err, res) => { 34 | if (err) { return done(err) } 35 | 36 | res.body.user.should.have.property('username') 37 | res.body.user.username.should.equal('supercoolname') 38 | expect(res.body.user.password).to.not.exist 39 | 40 | context.user = res.body.user 41 | context.token = res.headers.authorization 42 | 43 | done() 44 | }) 45 | }) 46 | }) 47 | 48 | describe('POST /v1/auth', () => { 49 | it('should throw 401 if credentials are incorrect', (done) => { 50 | request 51 | .post('/v1/auth') 52 | .set('Accept', 'application/json') 53 | .send({ username: 'supercoolname', password: 'wrongpassword' }) 54 | .expect(401, done) 55 | }) 56 | 57 | it('should auth user', (done) => { 58 | request 59 | .post('/v1/auth') 60 | .set('Accept', 'application/json') 61 | .send({ username: 'supercoolname', password: 'supersecretpassword' }) 62 | .expect(200, (err, res) => { 63 | if (err) { return done(err) } 64 | 65 | res.body.user.should.have.property('username') 66 | res.body.user.username.should.equal('supercoolname') 67 | expect(res.body.user.password).to.not.exist 68 | 69 | context.user = res.body.user 70 | context.token = res.headers.authorization 71 | 72 | done() 73 | }) 74 | }) 75 | }) 76 | 77 | describe('GET /v1/users', () => { 78 | it('should not fetch users if the authorization header is missing', (done) => { 79 | request 80 | .get('/v1/users') 81 | .set('Accept', 'application/json') 82 | .expect(401, done) 83 | }) 84 | 85 | it('should not fetch users if the authorization header is missing the scheme', (done) => { 86 | request 87 | .get('/v1/users') 88 | .set({ 89 | Accept: 'application/json', 90 | Authorization: '1' 91 | }) 92 | .expect(401, done) 93 | }) 94 | 95 | it('should not fetch users if the authorization header has invalid scheme', (done) => { 96 | const { token } = context 97 | request 98 | .get('/v1/users') 99 | .set({ 100 | Accept: 'application/json', 101 | Authorization: `Unknown ${token}` 102 | }) 103 | .expect(401, done) 104 | }) 105 | 106 | it('should not fetch users if token is invalid', (done) => { 107 | request 108 | .get('/v1/users') 109 | .set({ 110 | Accept: 'application/json', 111 | Authorization: 'Bearer 1' 112 | }) 113 | .expect(401, done) 114 | }) 115 | 116 | it('should fetch all users', (done) => { 117 | const { token } = context 118 | request 119 | .get('/v1/users') 120 | .set({ 121 | Accept: 'application/json', 122 | Authorization: `Bearer ${token}` 123 | }) 124 | .expect(200, (err, res) => { 125 | if (err) { return done(err) } 126 | 127 | res.body.should.have.property('users') 128 | 129 | res.body.users.should.have.length(1) 130 | 131 | done() 132 | }) 133 | }) 134 | }) 135 | 136 | describe('POST /v2/auth', () => { 137 | it('should throw 401 if credentials are incorrect', (done) => { 138 | request 139 | .post('/v1/auth') 140 | .set('Accept', 'application/json') 141 | .send({ username: 'supercoolname', password: 'wrongpassword' }) 142 | .expect(401, done) 143 | }) 144 | 145 | it('should auth user', (done) => { 146 | request 147 | .post('/v2/auth') 148 | .set('Accept', 'application/json') 149 | .send({ username: 'supercoolname', password: 'supersecretpassword' }) 150 | .expect(200, (err, res) => { 151 | if (err) { return done(err) } 152 | 153 | res.body.user.should.have.property('username') 154 | res.body.user.username.should.equal('supercoolname') 155 | expect(res.body.user.password).to.not.exist 156 | 157 | context.user = res.body.user 158 | context.token = res.headers.authorization 159 | 160 | done() 161 | }) 162 | }) 163 | }) 164 | 165 | describe('GET /v2/users', () => { 166 | it('should not fetch users if the authorization header is missing for v2', (done) => { 167 | request 168 | .get('/v1/users') 169 | .set('Accept', 'application/json') 170 | .expect(401, done) 171 | }) 172 | 173 | it('should not fetch users if the authorization header is missing the scheme for v2', (done) => { 174 | request 175 | .get('/v2/users') 176 | .set({ 177 | Accept: 'application/json', 178 | Authorization: '1' 179 | }) 180 | .expect(401, done) 181 | }) 182 | 183 | it('should not fetch users if the authorization header has invalid scheme for v2', (done) => { 184 | const { token } = context 185 | request 186 | .get('/v2/users') 187 | .set({ 188 | Accept: 'application/json', 189 | Authorization: `Unknown ${token}` 190 | }) 191 | .expect(401, done) 192 | }) 193 | 194 | it('should not fetch users if token is invalid for v2', (done) => { 195 | request 196 | .get('/v2/users') 197 | .set({ 198 | Accept: 'application/json', 199 | Authorization: 'Bearer 1' 200 | }) 201 | .expect(401, done) 202 | }) 203 | 204 | it('should fetch all users', (done) => { 205 | const { token } = context 206 | request 207 | .get('/v2/users') 208 | .set({ 209 | Accept: 'application/json', 210 | Authorization: `Bearer ${token}` 211 | }) 212 | .expect(200, (err, res) => { 213 | if (err) { return done(err) } 214 | 215 | res.body.should.have.property('users') 216 | 217 | res.body.users.should.have.length(1) 218 | 219 | done() 220 | }) 221 | }) 222 | }) 223 | 224 | describe('GET /v1/users/:id', () => { 225 | it('should not fetch user if token is invalid', (done) => { 226 | request 227 | .get('/v1/users/1') 228 | .set({ 229 | Accept: 'application/json', 230 | Authorization: 'Bearer 1' 231 | }) 232 | .expect(401, done) 233 | }) 234 | 235 | it('should fetch user', (done) => { 236 | const { 237 | user: { _id }, 238 | token 239 | } = context 240 | 241 | request 242 | .get(`/v1/users/${_id}`) 243 | .set({ 244 | Accept: 'application/json', 245 | Authorization: `Bearer ${token}` 246 | }) 247 | .expect(200, (err, res) => { 248 | if (err) { return done(err) } 249 | 250 | res.body.should.have.property('user') 251 | 252 | expect(res.body.user.password).to.not.exist 253 | 254 | done() 255 | }) 256 | }) 257 | }) 258 | 259 | describe('PUT /users/:id', () => { 260 | it('should not update user if token is invalid', (done) => { 261 | request 262 | .put('/v1/users/1') 263 | .set({ 264 | Accept: 'application/json', 265 | Authorization: 'Bearer 1' 266 | }) 267 | .expect(401, done) 268 | }) 269 | 270 | it('should update user', (done) => { 271 | const { 272 | user: { _id }, 273 | token 274 | } = context 275 | 276 | request 277 | .put(`/v1/users/${_id}`) 278 | .set({ 279 | Accept: 'application/json', 280 | Authorization: `Bearer ${token}` 281 | }) 282 | .send({ user: { username: 'updatedcoolname' } }) 283 | .expect(201, (err, res) => { 284 | if (err) { return done(err) } 285 | done() 286 | }) 287 | }) 288 | }) 289 | 290 | describe('DELETE /v1/users/:id', () => { 291 | it('should not delete user if token is invalid', (done) => { 292 | request 293 | .delete('/v1/users/1') 294 | .set({ 295 | Accept: 'application/json', 296 | Authorization: 'Bearer 1' 297 | }) 298 | .expect(401, done) 299 | }) 300 | 301 | it('should delete user', (done) => { 302 | const { 303 | user: { _id }, 304 | token 305 | } = context 306 | 307 | request 308 | .delete(`/v1/users/${_id}`) 309 | .set({ 310 | Accept: 'application/json', 311 | Authorization: `Bearer ${token}` 312 | }) 313 | .expect(200, done) 314 | }) 315 | }) 316 | }) 317 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | export function cleanDb () { 4 | for (const collection in mongoose.connection.collections) { 5 | if (mongoose.connection.collections.hasOwnProperty(collection)) { 6 | mongoose.connection.collections[collection].remove() 7 | } 8 | } 9 | } 10 | 11 | export function authUser (agent, callback) { 12 | agent 13 | .post('/v1/users') 14 | .set('Accept', 'application/json') 15 | .send({ user: { username: 'test', password: 'pass' } }) 16 | .end((err, res) => { 17 | if (err) { return callback(err) } 18 | 19 | callback(null, { 20 | user: res.body.user, 21 | token: res.body.token 22 | }) 23 | }) 24 | } 25 | --------------------------------------------------------------------------------