├── .editorconfig ├── .eslintignore ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .yo-rc.json ├── LICENSE ├── README.md ├── __tests__ └── app.js ├── generators ├── app │ ├── index.js │ └── templates │ │ ├── .editorconfig │ │ ├── LICENSE │ │ ├── _.env │ │ ├── _README.md │ │ ├── _eslintrc.json │ │ ├── _nodemon.json │ │ ├── _package.json │ │ ├── _swaggerDef.js │ │ ├── _tsconfig.json │ │ ├── src │ │ ├── components │ │ │ ├── Auth │ │ │ │ ├── index.ts │ │ │ │ ├── interface.ts │ │ │ │ ├── service.ts │ │ │ │ └── validation.ts │ │ │ ├── User │ │ │ │ ├── index.ts │ │ │ │ ├── interface.ts │ │ │ │ ├── model.ts │ │ │ │ ├── service.ts │ │ │ │ └── validation.ts │ │ │ ├── index.ts │ │ │ └── validation.ts │ │ ├── config │ │ │ ├── connection │ │ │ │ └── connection.ts │ │ │ ├── env │ │ │ │ └── index.ts │ │ │ ├── error │ │ │ │ ├── index.ts │ │ │ │ └── sendHttpError.ts │ │ │ ├── middleware │ │ │ │ └── middleware.ts │ │ │ └── server │ │ │ │ ├── index.ts │ │ │ │ ├── server.ts │ │ │ │ └── serverHandlers.ts │ │ └── routes │ │ │ ├── AuthRouter.ts │ │ │ ├── UserRouter.ts │ │ │ └── index.ts │ │ └── test │ │ ├── api.js │ │ ├── authentication.js │ │ ├── fixtures │ │ ├── OAuthClient.json │ │ ├── OAuthToken.js │ │ └── user.json │ │ └── index.js └── authentication │ ├── index.js │ └── templates │ └── src │ └── config │ ├── middleware │ ├── jwtAuth.ts │ ├── oAuth.ts │ └── passport.ts │ └── oauth │ ├── authCodeModel.ts │ ├── clientModel.ts │ ├── index.ts │ └── tokenModel.ts └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | **/templates 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | logs 4 | *.log 5 | node_modules 6 | .DS_STORE 7 | .netbeans 8 | .idea 9 | .vscode 10 | .node_history 11 | .vscode 12 | build 13 | package-lock.json 14 | .sonarlint 15 | .history 16 | .sonarlint -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v10 4 | - v8 5 | - v6 6 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-node": { 3 | "promptValues": { 4 | "authorName": "Checha Valerii", 5 | "authorEmail": "chechavalera@gmail.com", 6 | "authorUrl": "https://github.com/ChechaValerii" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Checha Valerii (https://github.com/ChechaValerii) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/chechavalerii) 2 | 3 | # Node.js Express API with TypeScript 4 4 | 5 | 6 | ![CircleCI branch](https://img.shields.io/circleci/project/github/RedSparr0w/node-csgo-parser/master.svg?style=flat-square) 7 | ![npm](https://img.shields.io/npm/dm/localeval.svg?style=flat-square) 8 | ![Plugin on redmine.org](https://img.shields.io/redmine/plugin/stars/redmine_xlsx_format_issue_exporter.svg?style=flat-square) 9 | ![onix](https://img.shields.io/badge/onix-systems-blue.svg) 10 | 11 | > Node.js Express API with TypeScript 4. Supports MongoDB 12 | > See [node-express-fast-progress](https://github.com/ChechaValerii/node-express-fast-progress) if you need vanilla JS 13 | 14 | ## Description 15 | This generator will help you to build your own Node.js Express Mongodb API using TypeScript 4. 16 | 17 | ### Project Introduction 18 | - suppot ES6/ES7 features 19 | - using tslint followed [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) 20 | 21 | ## Features 22 | ##### Authentication: 23 | - passport local strategy 24 | - jwt authentication 25 | - OAuth2.0 Server (Authorization code grant, Refresh token grant) 26 | ##### Session Storage: 27 | - MongoDB 28 | - Redis 29 | ##### Integration testing 30 | - mocha 31 | - chai 32 | - supertest 33 | 34 | ## Requirements 35 | 36 | - node >= 14 37 | - npm >= 6 38 | - mongodb >= 4.0 39 | - typescript >= 4.0 40 | 41 | ## Installation 42 | 43 | First, install [Yeoman](http://yeoman.io) and generator-node-express-typescript-api using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). 44 | 45 | ```bash 46 | npm install -g yo 47 | npm install -g generator-node-express-typescript-api 48 | ``` 49 | 50 | Then generate your new project: 51 | 52 | ```bash 53 | yo node-express-typescript-api 54 | ``` 55 | ## App skeleton 56 | ``` 57 | . 58 | ├── LICENSE 59 | ├── README.md 60 | ├── nodemon.json 61 | ├── package.json 62 | ├── src 63 | │ ├── components 64 | │ │ ├── Auth 65 | │ │ │ ├── index.ts 66 | │ │ │ ├── interface.ts 67 | │ │ │ ├── service.ts 68 | │ │ │ └── validation.ts 69 | │ │ ├── User 70 | │ │ │ ├── index.ts 71 | │ │ │ ├── interface.ts 72 | │ │ │ ├── model.ts 73 | │ │ │ ├── service.ts 74 | │ │ │ └── validation.ts 75 | │ │ ├── index.ts 76 | │ │ └── validation.ts 77 | │ ├── config 78 | │ │ ├── connection 79 | │ │ │ └── connection.ts 80 | │ │ ├── env 81 | │ │ │ └── index.ts 82 | │ │ ├── error 83 | │ │ │ ├── index.ts 84 | │ │ │ └── sendHttpError.ts 85 | │ │ ├── middleware 86 | │ │ │ ├── middleware.ts 87 | │ │ │ └── passport.ts 88 | │ │ └── server 89 | │ │ ├── ServerInterface.ts 90 | │ │ ├── index.ts 91 | │ │ ├── server.ts 92 | │ │ └── serverHandlers.ts 93 | │ └── routes 94 | │ ├── AuthRouter.ts 95 | │ ├── UserRouter.ts 96 | │ └── index.ts 97 | ├── swagger.json 98 | ├── swaggerDef.js 99 | ├── tsconfig.json 100 | └── tslint.json 101 | ``` 102 | ## Running the API 103 | ### Development 104 | To start the application in development mode, run: 105 | 106 | ```bash 107 | npm install -g nodemon 108 | npm install -g ts-node 109 | npm install -g typescript 110 | npm install 111 | ``` 112 | 113 | Start the application in dev env: 114 | ``` 115 | nodemon 116 | ``` 117 | Start the application in production env: 118 | 119 | Install ts pm2 and typescript compiler: 120 | ``` 121 | npm install -g pm2 122 | pm2 install typescript 123 | ``` 124 | 125 | example start with scale on 2 core: 126 | ``` 127 | pm2 start ./src/index.ts -i 2 --no-daemon 128 | ``` 129 | 130 | Express server listening on http://localhost:3000/, in development mode 131 | The developer mode will watch your changes then will transpile the TypeScript code and re-run the node application automatically. 132 | 133 | ### Testing 134 | To run integration tests: 135 | ```bash 136 | npm test 137 | ``` 138 | 139 | ## Set up environment 140 | In root folder you can find `.env`. You can use this config or change it for your purposes. 141 | If you want to add some new variables, you also need to add them to interface and config object (Look `src/config/index.ts`) 142 | 143 | ## Usage as OAuth2.0 Server 144 | To use this generator as OAuth2.0 server you should implement client side, that will be handle your redirectUris and make requests to `/auth/token/` route. [Read more about OAuth2.0](https://alexbilbie.com/guide-to-oauth-2-grants/) 145 | 146 | ## Swagger 147 | ```bash 148 | npm install -g swagger-jsdoc 149 | swagger-jsdoc -d swaggerDef.js ./src/**/*.ts -o swagger.json 150 | ``` 151 | Swagger documentation will be available on route: 152 | ```bash 153 | http://localhost:3000/docs 154 | ``` 155 | ![Alt Text](https://i.ibb.co/b6SdyQV/gif1.gif) 156 | 157 | ## Getting To Know Yeoman 158 | 159 | * Yeoman has a heart of gold. 160 | * Yeoman is a person with feelings and opinions, but is very easy to work with. 161 | * Yeoman can be too opinionated at times but is easily convinced not to be. 162 | * Feel free to [learn more about Yeoman](http://yeoman.io/). 163 | 164 | [travis-image]: https://travis-ci.org/caiobsouza/generator-ts-node-api.svg?branch=master 165 | [travis-url]: https://travis-ci.org/caiobsouza/generator-ts-node-api 166 | -------------------------------------------------------------------------------- /__tests__/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const assert = require('yeoman-assert'); 4 | const helpers = require('yeoman-test'); 5 | 6 | describe('generator-node-express-typescript-api:app', () => { 7 | beforeAll(() => { 8 | return helpers 9 | .run(path.join(__dirname, '../generators/app')) 10 | .withPrompts({ someAnswer: true }); 11 | }); 12 | 13 | it('creates files', () => { 14 | assert.file(['dummyfile.txt']); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /generators/app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Generator = require('yeoman-generator'); 3 | const chalk = require('chalk'); 4 | const yosay = require('yosay'); 5 | const crypto = require('crypto'); 6 | 7 | const whenAuthIsChosen = authChoises => props => 8 | authChoises.indexOf(props.authentication) !== -1; 9 | const whenSessionIs = inputs => props => { 10 | if ( 11 | inputs && 12 | props.authentication.includes('passport-local-strategy') && 13 | props.sessionStore.includes('redis') 14 | ) { 15 | return true; 16 | } 17 | return false; 18 | }; 19 | module.exports = class extends Generator { 20 | initializing() {} 21 | 22 | prompting() { 23 | // Have Yeoman greet the user. 24 | this.log( 25 | yosay( 26 | `Welcome to the finest ${chalk.red( 27 | 'generator-node-express-typescript-api' 28 | )} generator!` 29 | ) 30 | ); 31 | 32 | const prompts = [ 33 | { 34 | type: 'input', 35 | name: 'name', 36 | message: 'Would you like to root name to be called?', 37 | default: 'my-project' 38 | }, 39 | { 40 | type: 'list', 41 | name: 'authentication', 42 | message: 'Choose authentication type', 43 | default: 'passport-local-strategy', 44 | choices: ['passport-local-strategy', 'jwt-auth', 'oauth2.0'] 45 | }, 46 | { 47 | type: 'input', 48 | name: 'authentication:client_id', 49 | message: 'Enter your client_id', 50 | default: crypto.randomBytes(20).toString('hex'), 51 | when: whenAuthIsChosen(['oauth2.0']) 52 | }, 53 | { 54 | type: 'input', 55 | name: 'authentication:client_secret', 56 | message: 'Enter your client_secret', 57 | default: crypto.randomBytes(20).toString('hex'), 58 | when: whenAuthIsChosen(['oauth2.0']) 59 | }, 60 | { 61 | type: 'input', 62 | name: 'authentication:redirect_uris', 63 | suffix: 64 | '(if you want to add more than one redirect uri, please, separate them by comma)', 65 | message: 66 | 'Enter your client(application) redirect uris, that handle authorization_code and make request to /auth/token ', 67 | default: 'http://localhost:3001/oauthExample/callback', 68 | when: whenAuthIsChosen(['oauth2.0']) 69 | }, 70 | { 71 | type: 'input', 72 | name: 'authentication:secret', 73 | message: 'Enter your authentication secret key', 74 | default: crypto.randomBytes(20).toString('hex'), 75 | when: whenAuthIsChosen(['jwt-auth', 'passport-local-strategy']) 76 | }, 77 | { 78 | type: 'list', 79 | name: 'sessionStore', 80 | message: 'Choos session store', 81 | default: 'mongo', 82 | when: whenAuthIsChosen(['passport-local-strategy']), 83 | choices: ['mongo', 'redis'] 84 | }, 85 | { 86 | type: 'input', 87 | name: 'redis:port', 88 | message: 'redis port: ', 89 | default: 6379, 90 | when: whenSessionIs('redis') 91 | }, 92 | { 93 | type: 'input', 94 | name: 'redis:host', 95 | message: 'redis host: ', 96 | default: '127.0.0.1', 97 | when: whenSessionIs('redis') 98 | } 99 | ]; 100 | 101 | return this.prompt(prompts).then(props => { 102 | // To access props later use this.props.someAnswer; 103 | this.name = props.name; 104 | this.authentication = props.authentication; 105 | this.secret = props['authentication:secret']; 106 | this.clientId = props['authentication:client_id']; 107 | this.clientSecret = props['authentication:client_secret']; 108 | this.redirectUris = props['authentication:redirect_uris'] 109 | ? props['authentication:redirect_uris'].split(',') 110 | : ''; 111 | this.sessionStore = props.sessionStore; 112 | this.redisPort = props['redis:port']; 113 | this.redisHost = props['redis:host']; 114 | }); 115 | } 116 | 117 | configuring() {} 118 | 119 | default() { 120 | if (this.authentication) { 121 | this.composeWith('node-express-typescript-api:authentication', { 122 | name: this.name, 123 | authentication: this.authentication, 124 | clientId: this.clientId, 125 | clientSecret: this.clientSecret, 126 | redirectUris: this.redirectUris 127 | }); 128 | } 129 | } 130 | 131 | writing() { 132 | this.fs.copyTpl( 133 | this.templatePath('_.env'), 134 | this.destinationPath(this.name + '/.env') 135 | ); 136 | 137 | this.fs.copyTpl( 138 | this.templatePath('_swaggerDef.js'), 139 | this.destinationPath(this.name + '/swaggerDef.js') 140 | ); 141 | 142 | this.fs.copyTpl( 143 | this.templatePath('_nodemon.json'), 144 | this.destinationPath(this.name + '/nodemon.json') 145 | ); 146 | 147 | this.fs.copyTpl( 148 | this.templatePath('_package.json'), 149 | this.destinationPath(this.name + '/package.json') 150 | ); 151 | 152 | this.fs.copyTpl( 153 | this.templatePath('_tsconfig.json'), 154 | this.destinationPath(this.name + '/tsconfig.json') 155 | ); 156 | 157 | this.fs.copyTpl( 158 | this.templatePath('_README.md'), 159 | this.destinationPath(this.name + '/README.md') 160 | ); 161 | 162 | this.fs.copyTpl( 163 | this.templatePath('_eslintrc.json'), 164 | this.destinationPath(this.name + '/.eslintrc.json') 165 | ); 166 | 167 | this.fs.copyTpl( 168 | this.templatePath('LICENSE'), 169 | this.destinationPath(this.name + '/LICENSE') 170 | ); 171 | 172 | this.fs.copyTpl( 173 | this.templatePath('src/'), 174 | this.destinationPath(this.name + '/src/'), 175 | { 176 | name: this.name, 177 | authentication: this.authentication, 178 | sessionStore: this.sessionStore 179 | } 180 | ); 181 | 182 | if (this.authentication === 'oauth2.0') { 183 | this.fs.copyTpl( 184 | this.templatePath('test/'), 185 | this.destinationPath(this.name + '/test/'), 186 | { 187 | authentication: this.authentication 188 | } 189 | ); 190 | } else { 191 | this.fs.copyTpl( 192 | this.templatePath('test/**/user.json'), 193 | this.destinationPath(this.name + '/test/'), 194 | { 195 | authentication: this.authentication 196 | } 197 | ); 198 | 199 | this.fs.copyTpl( 200 | this.templatePath('test/*.js'), 201 | this.destinationPath(this.name + '/test/'), 202 | { 203 | authentication: this.authentication 204 | } 205 | ); 206 | } 207 | 208 | if (this.secret) { 209 | this.fs.append(this.name + '/.env', '\nSECRET=' + this.secret); 210 | } 211 | 212 | if (this.redisPort && this.redisHost) { 213 | this.fs.append( 214 | this.name + '/.env', 215 | '\nREDIS_PORT=' + this.redisPort + '\nREDIS_HOST=' + this.redisHost 216 | ); 217 | } 218 | 219 | if (this.authentication === 'oauth2.0') { 220 | const pkgJson = { 221 | devDependencies: { 222 | '@types/oauth2-server': '3.0.13' 223 | }, 224 | dependencies: { 225 | 'oauth2-server': '3.1.1' 226 | } 227 | }; 228 | 229 | // Extend or create package.json file in destination path 230 | this.fs.extendJSON( 231 | this.destinationPath(this.name + '/package.json'), 232 | pkgJson 233 | ); 234 | } 235 | 236 | if (this.authentication === 'passport-local-strategy') { 237 | const pkgJson = { 238 | devDependencies: { 239 | '@types/express-session': '1.17.4', 240 | '@types/passport-local': '1.0.34', 241 | '@types/passport': '1.0.7' 242 | }, 243 | dependencies: { 244 | passport: '0.4.1', 245 | 'passport-local': '1.0.0', 246 | 'express-session': '1.17.2', 247 | 'connect-mongo': '4.5.0' 248 | } 249 | }; 250 | if (this.sessionStore === 'mongo') { 251 | pkgJson.devDependencies['@types/connect-mongo'] = '3.1.3'; 252 | pkgJson.dependencies['connect-mongo'] = '4.5.0'; 253 | } 254 | 255 | if (this.sessionStore === 'redis') { 256 | pkgJson.dependencies.ioredis = '5.0.5'; 257 | pkgJson.devDependencies['@types/connect-redis'] = '0.0.17'; 258 | pkgJson.dependencies['connect-redis'] = '6.0.0'; 259 | } 260 | 261 | // Extend or create package.json file in destination path 262 | this.fs.extendJSON( 263 | this.destinationPath(this.name + '/package.json'), 264 | pkgJson 265 | ); 266 | } 267 | 268 | if (this.authentication === 'jwt-auth') { 269 | const pkgJson = { 270 | devDependencies: { 271 | '@types/jsonwebtoken': '8.5.5' 272 | }, 273 | dependencies: { 274 | jsonwebtoken: '8.5.1' 275 | } 276 | }; 277 | 278 | // Extend or create package.json file in destination path 279 | this.fs.extendJSON( 280 | this.destinationPath(this.name + '/package.json'), 281 | pkgJson 282 | ); 283 | } 284 | } 285 | 286 | conflicts() {} 287 | 288 | install() {} 289 | 290 | end() {} 291 | }; 292 | -------------------------------------------------------------------------------- /generators/app/templates/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /generators/app/templates/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Checha Valerii (https://github.com/ChechaValerii) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /generators/app/templates/_.env: -------------------------------------------------------------------------------- 1 | #can be 'development' or 'production' 2 | NODE_ENV=development 3 | 4 | #your app port 5 | PORT=3000 6 | 7 | #your mongo connection options 8 | MONGODB_URI=mongodb://127.0.0.1:27017/ 9 | MONGODB_DB_MAIN=users_db 10 | -------------------------------------------------------------------------------- /generators/app/templates/_README.md: -------------------------------------------------------------------------------- 1 | # Node.js Express API with TypeScript 4 2 | 3 | 4 | ![CircleCI branch](https://img.shields.io/circleci/project/github/RedSparr0w/node-csgo-parser/master.svg?style=flat-square) 5 | ![npm](https://img.shields.io/npm/dm/localeval.svg?style=flat-square) 6 | ![Plugin on redmine.org](https://img.shields.io/redmine/plugin/stars/redmine_xlsx_format_issue_exporter.svg?style=flat-square) 7 | ![onix](https://img.shields.io/badge/onix-systems-blue.svg) 8 | 9 | > Node.js Express API with TypeScript 4. Supports MongoDB 10 | 11 | ## Description 12 | This generator will help you to build your own Node.js Express Mongodb API using TypeScript 4. 13 | 14 | ### Project Introduction 15 | - suppot ES6/ES7 features 16 | - using tslint followed [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) 17 | 18 | ## Features 19 | ##### Authentication: 20 | - passport local strategy 21 | - jwt authentication 22 | - OAuth2.0 Server (Authorization code grant, Refresh token grant) 23 | ##### Session Storage: 24 | - MongoDB 25 | - Redis 26 | ##### Integration testing 27 | - mocha 28 | - chai 29 | - supertest 30 | 31 | ## Requirements 32 | 33 | - node >= 14 34 | - npm >= 6 35 | - mongodb >= 4.0 36 | - typescript >= 4.0 37 | 38 | ## Installation 39 | 40 | First, install [Yeoman](http://yeoman.io) and generator-node-express-typescript-api using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). 41 | 42 | ```bash 43 | npm install -g yo 44 | npm install -g generator-node-express-typescript-api 45 | ``` 46 | 47 | Then generate your new project: 48 | 49 | ```bash 50 | yo node-express-typescript-api 51 | ``` 52 | ## App skeleton 53 | ``` 54 | . 55 | ├── LICENSE 56 | ├── README.md 57 | ├── nodemon.json 58 | ├── package.json 59 | ├── src 60 | │ ├── components 61 | │ │ ├── Auth 62 | │ │ │ ├── index.ts 63 | │ │ │ ├── interface.ts 64 | │ │ │ ├── service.ts 65 | │ │ │ └── validation.ts 66 | │ │ ├── User 67 | │ │ │ ├── index.ts 68 | │ │ │ ├── interface.ts 69 | │ │ │ ├── model.ts 70 | │ │ │ ├── service.ts 71 | │ │ │ └── validation.ts 72 | │ │ ├── index.ts 73 | │ │ └── validation.ts 74 | │ ├── config 75 | │ │ ├── connection 76 | │ │ │ └── connection.ts 77 | │ │ ├── env 78 | │ │ │ └── index.ts 79 | │ │ ├── error 80 | │ │ │ ├── index.ts 81 | │ │ │ └── sendHttpError.ts 82 | │ │ ├── middleware 83 | │ │ │ ├── middleware.ts 84 | │ │ │ └── passport.ts 85 | │ │ └── server 86 | │ │ ├── ServerInterface.ts 87 | │ │ ├── index.ts 88 | │ │ ├── server.ts 89 | │ │ └── serverHandlers.ts 90 | │ └── routes 91 | │ ├── AuthRouter.ts 92 | │ ├── UserRouter.ts 93 | │ └── index.ts 94 | ├── swaggerDef.js 95 | ├── tsconfig.json 96 | └── .eslintrc.json 97 | ``` 98 | ## Running the API 99 | ### Development 100 | To start the application in development mode, run: 101 | 102 | ```bash 103 | npm install -g nodemon 104 | npm install -g ts-node 105 | npm install -g typescript 106 | npm install 107 | ``` 108 | 109 | Start the application in dev env: 110 | ``` 111 | nodemon 112 | ``` 113 | Start the application in production env: 114 | 115 | Install ts pm2 and typescript compiler: 116 | ``` 117 | npm install -g pm2 118 | pm2 install typescript 119 | ``` 120 | 121 | example start with scale on 2 core: 122 | ``` 123 | pm2 start ./src/index.ts -i 2 --no-daemon 124 | ``` 125 | 126 | Express server listening on http://localhost:3000/, in development mode 127 | The developer mode will watch your changes then will transpile the TypeScript code and re-run the node application automatically. 128 | 129 | ### Testing 130 | To run integration tests: 131 | ```bash 132 | npm test 133 | ``` 134 | 135 | ## Set up environment 136 | In root folder you can find `.env`. You can use this config or change it for your purposes. 137 | If you want to add some new variables, you also need to add them to interface and config object (Look `src/config/index.ts`) 138 | 139 | ## Usage as OAuth2.0 Server 140 | To use this generator as OAuth2.0 server you should implement client side, that will be handle your redirectUris and make requests to `/auth/token/` route. [Read more about OAuth2.0](https://alexbilbie.com/guide-to-oauth-2-grants/) 141 | 142 | Swagger documentation will be available on route: 143 | ```bash 144 | http://localhost:3000/docs 145 | ``` 146 | ![Alt Text](https://i.ibb.co/b6SdyQV/gif1.gif) 147 | 148 | ## Getting To Know Yeoman 149 | 150 | * Yeoman has a heart of gold. 151 | * Yeoman is a person with feelings and opinions, but is very easy to work with. 152 | * Yeoman can be too opinionated at times but is easily convinced not to be. 153 | * Feel free to [learn more about Yeoman](http://yeoman.io/). 154 | 155 | [travis-image]: https://travis-ci.org/caiobsouza/generator-ts-node-api.svg?branch=master 156 | [travis-url]: https://travis-ci.org/caiobsouza/generator-ts-node-api 157 | -------------------------------------------------------------------------------- /generators/app/templates/_eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "airbnb-base" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaVersion": "latest", 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint" 16 | ], 17 | "rules": { 18 | "max-len": [ 19 | "error", 20 | 150 21 | ], 22 | "import/no-unresolved": "off", 23 | "no-useless-constructor": "off", 24 | "no-unused-vars": "off", 25 | "@typescript-eslint/no-unused-vars": [ 26 | "error" 27 | ], 28 | "padding-line-between-statements": [ 29 | "error", 30 | { "blankLine": "always", "prev": "*", "next": "return" } 31 | ], 32 | "no-param-reassign": "off", 33 | "import/extensions": "off", 34 | "no-empty-function": "off", 35 | "indent": [ 36 | "error", 37 | 4, 38 | { "SwitchCase": 1 } 39 | ], 40 | "import/no-extraneous-dependencies": "off", 41 | "import/prefer-default-export": "off", 42 | "func-names": "off", 43 | "consistent-return": "off", 44 | "arrow-body-style": "off", 45 | "class-methods-use-this": "off" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /generators/app/templates/_nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "exec": "ts-node ./src/config/server/index.ts" 5 | } 6 | -------------------------------------------------------------------------------- /generators/app/templates/_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-express-rest-api", 3 | "version": "2.0.10", 4 | "main": "./build/index.js", 5 | "keywords": [ 6 | "yeoman-generator", 7 | "typescript", 8 | "node", 9 | "nodejs", 10 | "rest", 11 | "restfull", 12 | "api", 13 | "builder", 14 | "generator" 15 | ], 16 | "description": "This generator will help you to build your own Node.js Express Mongodb API using TypeScript.", 17 | "author": "Checha Valerii ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/ChechaValerii/node-typescript-mongodb/issues" 21 | }, 22 | "homepage": "https://github.com/ChechaValerii/node-typescript-mongodb#readme", 23 | "scripts": { 24 | "build": "tsc --skipLibCheck", 25 | "start": "tsc --skipLibCheck && node ./build/config/server/index.js", 26 | "ts:watch": "tsc -w", 27 | "test": "NODE_ENV=test mocha -r ts-node/register test/index.js --exit" 28 | }, 29 | "files": [ 30 | "src" 31 | ], 32 | "dependencies": { 33 | "bcrypt": "5.0.1", 34 | "body-parser": "1.19.1", 35 | "compression": "1.7.4", 36 | "cookie-parser": "1.4.6", 37 | "cors": "2.8.5", 38 | "dotenv": "14.1.0", 39 | "express": "4.17.2", 40 | "helmet": "4.6.0", 41 | "joi": "17.5.0", 42 | "mongoose": "6.1.6", 43 | "ts-node": "10.4.0", 44 | "util": "0.12.4" 45 | }, 46 | "devDependencies": { 47 | "@types/bcrypt": "5.0.0", 48 | "@types/body-parser": "1.19.2", 49 | "@types/chai": "4.3.0", 50 | "@types/compression": "1.7.2", 51 | "@types/cookie-parser": "1.4.2", 52 | "@types/cors": "2.8.12", 53 | "@types/debug": "4.1.7", 54 | "@types/dotenv": "8.2.0", 55 | "@types/ejs": "3.1.0", 56 | "@types/express": "4.17.13", 57 | "@types/helmet": "4.0.0", 58 | "@types/joi": "17.2.3", 59 | "@types/mongoose": "5.11.97", 60 | "@types/node": "17.0.9", 61 | "@types/supertest": "2.0.11", 62 | "@types/swagger-jsdoc": "6.0.1", 63 | "@types/swagger-ui-express": "4.1.3", 64 | "@types/yeoman-generator": "5.2.8", 65 | "@typescript-eslint/eslint-plugin": "5.26.0", 66 | "@typescript-eslint/parser": "5.26.0", 67 | "chai": "4.3.4", 68 | "eslint": "8.16.0", 69 | "eslint-config-airbnb-base": "15.0.0", 70 | "eslint-plugin-import": "2.26.0", 71 | "jsdoc": "3.6.7", 72 | "mocha": "9.1.4", 73 | "nodemon": "2.0.15", 74 | "source-map-loader": "3.0.1", 75 | "supertest": "6.2.1", 76 | "swagger-jsdoc": "6.1.0", 77 | "swagger-ui-express": "4.3.0", 78 | "typescript": "4.5.4" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /generators/app/templates/_swaggerDef.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | openapi: '3.0.0', 3 | info: { 4 | title: 'Node-Typescript API', 5 | version: '1.0.0', 6 | description: 'A sample API', 7 | }, 8 | servers: [ 9 | { url: 'http://localhost:3000' }, 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /generators/app/templates/_tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build/", 4 | "module": "commonjs", 5 | "allowSyntheticDefaultImports": true, 6 | "noImplicitAny": true, 7 | "target": "es6", 8 | "sourceMap": true, 9 | "plugins": [ 10 | { 11 | "name": "tslint-language-service" 12 | } 13 | ] 14 | }, 15 | "include": ["./src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/Auth/index.ts: -------------------------------------------------------------------------------- 1 | <%_ if(authentication === 'passport-local-strategy') { _%> 2 | import * as passport from 'passport'; 3 | <%_ }_%> 4 | import { NextFunction, Request, Response } from 'express'; 5 | <%_ if(authentication === 'jwt-auth') { _%> 6 | import * as jwt from 'jsonwebtoken'; 7 | import app from '../../config/server/server'; 8 | <%_ }_%> 9 | <%_ if(authentication === 'oauth2.0') { _%> 10 | import * as OAuth2Server from 'oauth2-server'; 11 | import oauth from '../../config/oauth'; 12 | <%_ }_%> 13 | import AuthService from './service'; 14 | import HttpError from '../../config/error'; 15 | import { IUserModel } from '../User/model'; 16 | <%_ if(authentication === 'passport-local-strategy') { _%> 17 | /** 18 | * 19 | * @param {Request} req 20 | * @param {Response} res 21 | * @param {NextFunction}next 22 | * @param {IUserModel} user 23 | * @param {string} resMessage 24 | */ 25 | function passportRequestLogin(req: Request, res: Response, next: NextFunction, user: IUserModel, resMessage: string): void { 26 | return req.logIn(user, (err) => { 27 | if (err) return next(new HttpError(err)); 28 | 29 | res.json({ 30 | status: 200, 31 | logged: true, 32 | message: resMessage, 33 | }); 34 | }); 35 | } 36 | <%_ }_%> 37 | 38 | /** 39 | * @export 40 | * @param {Request} req 41 | * @param {Response} res 42 | * @param {NextFunction} next 43 | * @returns {Promise < void >} 44 | */ 45 | export async function signup(req: Request, res: Response, next: NextFunction): Promise < void > { 46 | try { 47 | const user: IUserModel = await AuthService.createUser(req.body); 48 | <%_ if(authentication === 'jwt-auth') { _%> 49 | const token: string = jwt.sign({ email: user.email }, app.get('secret'), { 50 | expiresIn: '60m', 51 | }); 52 | 53 | res.json({ 54 | status: 200, 55 | logged: true, 56 | token, 57 | message: 'Sign in successfull', 58 | }); 59 | <%_ }_%> 60 | <%_ if(authentication === 'passport-local-strategy') { _%> 61 | 62 | passportRequestLogin(req, res, next, user, 'Sign in successfull'); 63 | <%_ }_%> 64 | <%_ if(authentication === 'oauth2.0') { _%> 65 | 66 | res.json({ 67 | status: 200, 68 | user: { 69 | email: user.email, 70 | }, 71 | }); 72 | <%_ }_%> 73 | } catch (error) { 74 | if (error.code === 500) { 75 | return next(new HttpError(error.message.status, error.message)); 76 | } 77 | res.json({ 78 | status: 400, 79 | message: error.message, 80 | }); 81 | } 82 | } 83 | 84 | /** 85 | * @export 86 | * @param {Request} req 87 | * @param {Response} res 88 | * @param {NextFunction} next 89 | * @returns {Promise < void >} 90 | */ 91 | export async function login(req: Request, res: Response, next: NextFunction): Promise < void > { 92 | <%_ if(authentication === 'passport-local-strategy') { _%> 93 | passport.authenticate('local', (err: Error, user: IUserModel) => { 94 | if (err) { 95 | return next(new HttpError(400, err.message)); 96 | } 97 | 98 | if (!user) { 99 | return res.json({ 100 | status: 401, 101 | logged: false, 102 | message: 'Invalid credentials!', 103 | }); 104 | } 105 | passportRequestLogin(req, res, next, user, 'Sign in successfull'); 106 | })(req, res, next); 107 | <%_ }_%> 108 | <%_ if(authentication === 'jwt-auth') { _%> 109 | try { 110 | const user: IUserModel = await AuthService.getUser(req.body); 111 | 112 | const token: string = jwt.sign({ email: user.email }, app.get('secret'), { 113 | expiresIn: '60m', 114 | }); 115 | 116 | res.json({ 117 | status: 200, 118 | logged: true, 119 | token, 120 | message: 'Sign in successfull', 121 | }); 122 | } catch (error) { 123 | if (error.code === 500) { 124 | return next(new HttpError(error.message.status, error.message)); 125 | } 126 | 127 | res.json({ 128 | status: 400, 129 | message: error.message, 130 | }); 131 | } 132 | <%_ }_%> 133 | <%_ if(authentication === 'oauth2.0') { _%> 134 | const reqOAuth: OAuth2Server.Request = new OAuth2Server.Request(req); 135 | const resOAuth: OAuth2Server.Response = new OAuth2Server.Response(res); 136 | 137 | const options: OAuth2Server.AuthorizeOptions = { 138 | authenticateHandler: { 139 | handle: async (request: Request): Promise => { 140 | try { 141 | const user: OAuth2Server.User = await AuthService.getUser(request.body); 142 | 143 | return user; 144 | } catch (error) { 145 | throw new Error(error); 146 | } 147 | }, 148 | }, 149 | }; 150 | const code: OAuth2Server.AuthorizationCode = await oauth.authorize(reqOAuth, resOAuth, options); 151 | 152 | res.redirect(`${code.redirectUri}?code=${code.authorizationCode}&state=${req.query.state}`); 153 | <%_ }_%> 154 | } 155 | <%_ if(authentication === 'passport-local-strategy') { _%> 156 | /** 157 | * @export 158 | * @param {Request} req 159 | * @param {Response} res 160 | * @param {NextFunction} next 161 | * @returns {Promise < void >} 162 | */ 163 | export async function logout(req: Request, res: Response): Promise < void > { 164 | if (!req.user) { 165 | res.json({ 166 | status: 401, 167 | logged: false, 168 | message: 'You are not authorized to app. Can\'t logout', 169 | }); 170 | } 171 | 172 | if (req.user) { 173 | req.logout(); 174 | res.json({ 175 | status: 200, 176 | logged: false, 177 | message: 'Successfuly logged out!', 178 | }); 179 | } 180 | } 181 | <%_ }_%> 182 | <%_ if(authentication === 'oauth2.0') { _%> 183 | /** 184 | * @param {Request} req 185 | * @param {Response} res 186 | * @param {NextFunction} next 187 | * @returns {Promise < void >} 188 | */ 189 | export async function token(req: Request, res: Response, next: NextFunction): Promise { 190 | try { 191 | const reqOAuth: OAuth2Server.Request = new OAuth2Server.Request(req); 192 | const resOAuth: OAuth2Server.Response = new OAuth2Server.Response(res); 193 | const oAuthToken: OAuth2Server.Token = await oauth.token(reqOAuth, resOAuth); 194 | 195 | res.json({ 196 | accessToken: oAuthToken.accessToken, 197 | refreshToken: oAuthToken.refreshToken, 198 | }); 199 | } catch (error) { 200 | return next(new HttpError(error.status, error.message)); 201 | } 202 | } 203 | <%_ }_%> 204 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/Auth/interface.ts: -------------------------------------------------------------------------------- 1 | import { IUserModel } from '../User/model'; 2 | 3 | /** 4 | * @export 5 | * @interaface IAuthService 6 | */ 7 | export interface IAuthService { 8 | /** 9 | * @param {IUserModel} userModel 10 | * @returns {Promise} 11 | * @memberof AuthService 12 | */ 13 | createUser(userModel: IUserModel): Promise < IUserModel > ; 14 | <%_ if(authentication === 'jwt-auth' || authentication === 'oauth2.0') { _%> 15 | /** 16 | * @param {IUserModel} userModel 17 | * @returns {Promise} 18 | * @memberof AuthService 19 | */ 20 | getUser(userModel: IUserModel): Promise < IUserModel >; 21 | <%_ }_%> 22 | } 23 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/Auth/service.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import AuthValidation from './validation'; 3 | import UserModel, { IUserModel } from '../User/model'; 4 | import { IAuthService } from './interface'; 5 | 6 | /** 7 | * @export 8 | * @implements {IAuthService} 9 | */ 10 | const AuthService: IAuthService = { 11 | /** 12 | * @param {IUserModel} body 13 | * @returns {Promise } 14 | * @memberof AuthService 15 | */ 16 | async createUser(body: IUserModel): Promise < IUserModel > { 17 | try { 18 | const validate: Joi.ValidationResult = AuthValidation.createUser(body); 19 | 20 | if (validate.error) { 21 | throw new Error(validate.error.message); 22 | } 23 | 24 | const user: IUserModel = new UserModel({ 25 | email: body.email, 26 | password: body.password, 27 | }); 28 | 29 | const query: IUserModel = await UserModel.findOne({ 30 | email: body.email, 31 | }); 32 | 33 | if (query) { 34 | throw new Error('This email already exists'); 35 | } 36 | 37 | const saved: IUserModel = await user.save(); 38 | 39 | return saved; 40 | } catch (error) { 41 | throw new Error(error); 42 | } 43 | }, 44 | <%_ if(authentication === 'jwt-auth' || authentication === 'oauth2.0') { _%> 45 | /** 46 | * @param {IUserModel} body 47 | * @returns {Promise } 48 | * @memberof AuthService 49 | */ 50 | async getUser(body: IUserModel): Promise < IUserModel > { 51 | try { 52 | const validate: Joi.ValidationResult = AuthValidation.getUser(body); 53 | 54 | if (validate.error) { 55 | throw new Error(validate.error.message); 56 | } 57 | 58 | const user: IUserModel = await UserModel.findOne({ 59 | email: body.email, 60 | }); 61 | 62 | const isMatched: boolean = user && await user.comparePassword(body.password); 63 | 64 | if (isMatched) { 65 | return user; 66 | } 67 | 68 | throw new Error('Invalid password or email'); 69 | } catch (error) { 70 | throw new Error(error); 71 | } 72 | }, 73 | <%_ }_%> 74 | }; 75 | 76 | export default AuthService; 77 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/Auth/validation.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import Validation from '../validation'; 3 | import { IUserModel } from '../User/model'; 4 | 5 | /** 6 | * @export 7 | * @class AuthValidation 8 | * @extends Validation 9 | */ 10 | class AuthValidation extends Validation { 11 | /** 12 | * Creates an instance of AuthValidation. 13 | * @memberof AuthValidation 14 | */ 15 | constructor() { 16 | super(); 17 | } 18 | 19 | /** 20 | * @param {IUserModel} params 21 | * @returns {Joi.ValidationResult} 22 | * @memberof UserValidation 23 | */ 24 | createUser( 25 | params: IUserModel, 26 | ): Joi.ValidationResult { 27 | const schema: Joi.Schema = Joi.object().keys({ 28 | password: Joi.string().required(), 29 | email: Joi.string().email({ 30 | minDomainSegments: 2, 31 | }).required(), 32 | }); 33 | 34 | return schema.validate(params); 35 | } 36 | <%_ if(authentication === 'jwt-auth' || authentication === 'oauth2.0') { _%> 37 | 38 | /** 39 | * @param {IUserModel} params 40 | * @returns {Joi.ValidationResult} 41 | * @memberof UserValidation 42 | */ 43 | getUser( 44 | params: IUserModel, 45 | ): Joi.ValidationResult { 46 | const schema: Joi.Schema = Joi.object().keys({ 47 | response_type: Joi.string(), 48 | state: Joi.string(), 49 | client_id: Joi.string(), 50 | password: Joi.string().required(), 51 | email: Joi.string().email({ 52 | minDomainSegments: 2, 53 | }).required(), 54 | }); 55 | 56 | return schema.validate(params); 57 | } 58 | <%_ }_%> 59 | } 60 | 61 | export default new AuthValidation(); 62 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/User/index.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import UserService from './service'; 3 | import { HttpError } from '../../config/error'; 4 | import { IUserModel } from './model'; 5 | 6 | /** 7 | * @export 8 | * @param {Request} req 9 | * @param {Response} res 10 | * @param {NextFunction} next 11 | * @returns {Promise < void >} 12 | */ 13 | export async function findAll(req: Request, res: Response, next: NextFunction): Promise < void > { 14 | try { 15 | const users: IUserModel[] = await UserService.findAll(); 16 | 17 | res.status(200).json(users); 18 | } catch (error) { 19 | next(new HttpError(error.message.status, error.message)); 20 | } 21 | } 22 | 23 | /** 24 | * @export 25 | * @param {Request} req 26 | * @param {Response} res 27 | * @param {NextFunction} next 28 | * @returns {Promise < void >} 29 | */ 30 | export async function findOne(req: Request, res: Response, next: NextFunction): Promise < void > { 31 | try { 32 | const user: IUserModel = await UserService.findOne(req.params.id); 33 | 34 | res.status(200).json(user); 35 | } catch (error) { 36 | next(new HttpError(error.message.status, error.message)); 37 | } 38 | } 39 | 40 | /** 41 | * @export 42 | * @param {Request} req 43 | * @param {Response} res 44 | * @param {NextFunction} next 45 | * @returns {Promise < void >} 46 | */ 47 | export async function create(req: Request, res: Response, next: NextFunction): Promise < void > { 48 | try { 49 | const user: IUserModel = await UserService.insert(req.body); 50 | 51 | res.status(201).json(user); 52 | } catch (error) { 53 | next(new HttpError(error.message.status, error.message)); 54 | } 55 | } 56 | 57 | /** 58 | * @export 59 | * @param {Request} req 60 | * @param {Response} res 61 | * @param {NextFunction} next 62 | * @returns {Promise < void >} 63 | */ 64 | export async function remove(req: Request, res: Response, next: NextFunction): Promise < void > { 65 | try { 66 | const user: IUserModel = await UserService.remove(req.params.id); 67 | 68 | res.status(200).json(user); 69 | } catch (error) { 70 | next(new HttpError(error.message.status, error.message)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/User/interface.ts: -------------------------------------------------------------------------------- 1 | import { IUserModel } from './model'; 2 | 3 | /** 4 | * @export 5 | * @interface IUserService 6 | */ 7 | export interface IUserService { 8 | 9 | /** 10 | * @returns {Promise} 11 | * @memberof IUserService 12 | */ 13 | findAll(): Promise; 14 | 15 | /** 16 | * @param {string} code 17 | * @returns {Promise} 18 | * @memberof IUserService 19 | */ 20 | findOne(code: string): Promise; 21 | 22 | /** 23 | * @param {IUserModel} userModel 24 | * @returns {Promise} 25 | * @memberof IUserService 26 | */ 27 | insert(userModel: IUserModel): Promise; 28 | 29 | /** 30 | * @param {string} id 31 | * @returns {Promise} 32 | * @memberof IUserService 33 | */ 34 | remove(id: string): Promise; 35 | } 36 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/User/model.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | import * as crypto from 'crypto'; 3 | import { Document, Schema } from 'mongoose'; 4 | import { NextFunction } from 'express'; 5 | import * as connections from '../../config/connection/connection'; 6 | 7 | export type AuthToken = { 8 | accessToken: string, 9 | kind: string, 10 | }; 11 | 12 | /** 13 | * @export 14 | * @interface IUserModel 15 | * @extends {Document} 16 | */ 17 | export interface IUserModel extends Document { 18 | email: string; 19 | password: string; 20 | passwordResetToken: string; 21 | passwordResetExpires: Date; 22 | 23 | facebook: string; 24 | tokens: AuthToken[]; 25 | 26 | profile: { 27 | name: string, 28 | gender: string, 29 | location: string, 30 | website: string, 31 | picture: string, 32 | }; 33 | comparePassword: (password: string) => Promise < boolean > ; 34 | gravatar: (size: number) => string; 35 | } 36 | 37 | /** 38 | * @swagger 39 | * components: 40 | * schemas: 41 | * UserSchema: 42 | * required: 43 | * - email 44 | * - name 45 | * properties: 46 | * id: 47 | * type: string 48 | * name: 49 | * type: string 50 | * email: 51 | * type: string 52 | * password: 53 | * type: string 54 | * passwordResetToken: 55 | * type: string 56 | * passwordResetExpires: 57 | * type: string 58 | * format: date 59 | * tokens: 60 | * type: array 61 | * Users: 62 | * type: array 63 | * items: 64 | * $ref: '#/components/schemas/UserSchema' 65 | */ 66 | const UserSchema: Schema = new Schema({ 67 | email: { 68 | type: String, 69 | unique: true, 70 | trim: true, 71 | }, 72 | password: String, 73 | passwordResetToken: String, 74 | passwordResetExpires: Date, 75 | tokens: Array, 76 | }, { 77 | collection: 'usermodel', 78 | versionKey: false, 79 | }).pre('save', async function (next: NextFunction): Promise < void > { 80 | const user: IUserModel = this; // tslint:disable-line 81 | 82 | if (!user.isModified('password')) { 83 | return next(); 84 | } 85 | 86 | try { 87 | const salt: string = await bcrypt.genSalt(10); 88 | 89 | const hash: string = await bcrypt.hash(user.password, salt); 90 | 91 | user.password = hash; 92 | next(); 93 | } catch (error) { 94 | return next(error); 95 | } 96 | }); 97 | 98 | /** 99 | * Method for comparing passwords 100 | */ 101 | UserSchema.methods.comparePassword = async function (candidatePassword: string): Promise < boolean > { 102 | try { 103 | const match: boolean = await bcrypt.compare(candidatePassword, this.password); 104 | 105 | return match; 106 | } catch (error) { 107 | return error; 108 | } 109 | }; 110 | 111 | /** 112 | * Helper method for getting user's gravatar. 113 | */ 114 | UserSchema.methods.gravatar = function (size: number): string { 115 | if (!size) { 116 | size = 200; 117 | } 118 | if (!this.email) { 119 | return `https://gravatar.com/avatar/?s=${size}&d=retro`; 120 | } 121 | const md5: string = crypto.createHash('md5').update(this.email).digest('hex'); 122 | 123 | return `https://gravatar.com/avatar/${md5}?s=${size}&d=retro`; 124 | }; 125 | 126 | export default connections.db.model< IUserModel >('UserModel', UserSchema); 127 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/User/service.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import { Types } from 'mongoose'; 3 | import UserModel, { IUserModel } from './model'; 4 | import UserValidation from './validation'; 5 | import { IUserService } from './interface'; 6 | 7 | /** 8 | * @export 9 | * @implements {IUserModelService} 10 | */ 11 | const UserService: IUserService = { 12 | /** 13 | * @returns {Promise < IUserModel[] >} 14 | * @memberof UserService 15 | */ 16 | async findAll(): Promise < IUserModel[] > { 17 | try { 18 | return await UserModel.find({}); 19 | } catch (error) { 20 | throw new Error(error.message); 21 | } 22 | }, 23 | 24 | /** 25 | * @param {string} id 26 | * @returns {Promise < IUserModel >} 27 | * @memberof UserService 28 | */ 29 | async findOne(id: string): Promise < IUserModel > { 30 | try { 31 | const validate: Joi.ValidationResult = UserValidation.getUser({ 32 | id, 33 | }); 34 | 35 | if (validate.error) { 36 | throw new Error(validate.error.message); 37 | } 38 | 39 | return await UserModel.findOne({ 40 | _id: new Types.ObjectId(id), 41 | }); 42 | } catch (error) { 43 | throw new Error(error.message); 44 | } 45 | }, 46 | 47 | /** 48 | * @param {IUserModel} user 49 | * @returns {Promise < IUserModel >} 50 | * @memberof UserService 51 | */ 52 | async insert(body: IUserModel): Promise < IUserModel > { 53 | try { 54 | const validate: Joi.ValidationResult = UserValidation.createUser(body); 55 | 56 | if (validate.error) { 57 | throw new Error(validate.error.message); 58 | } 59 | 60 | const user: IUserModel = await UserModel.create(body); 61 | 62 | return user; 63 | } catch (error) { 64 | throw new Error(error.message); 65 | } 66 | }, 67 | 68 | /** 69 | * @param {string} id 70 | * @returns {Promise < IUserModel >} 71 | * @memberof UserService 72 | */ 73 | async remove(id: string): Promise < IUserModel > { 74 | try { 75 | const validate: Joi.ValidationResult = UserValidation.removeUser({ 76 | id, 77 | }); 78 | 79 | if (validate.error) { 80 | throw new Error(validate.error.message); 81 | } 82 | 83 | const user: IUserModel = await UserModel.findOneAndRemove({ 84 | _id: new Types.ObjectId(id), 85 | }); 86 | 87 | return user; 88 | } catch (error) { 89 | throw new Error(error.message); 90 | } 91 | }, 92 | }; 93 | 94 | export default UserService; 95 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/User/validation.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import Validation from '../validation'; 3 | import { IUserModel } from './model'; 4 | 5 | /** 6 | * @export 7 | * @class UserValidation 8 | * @extends Validation 9 | */ 10 | class UserValidation extends Validation { 11 | /** 12 | * Creates an instance of UserValidation. 13 | * @memberof UserValidation 14 | */ 15 | constructor() { 16 | super(); 17 | } 18 | 19 | /** 20 | * @param {IUserModel} params 21 | * @returns {Joi.ValidationResult} 22 | * @memberof UserValidation 23 | */ 24 | createUser( 25 | params: IUserModel, 26 | ): Joi.ValidationResult { 27 | const schema: Joi.Schema = Joi.object().keys({ 28 | name: Joi.string().required(), 29 | email: Joi.string().email({ 30 | minDomainSegments: 2, 31 | }).required(), 32 | }); 33 | 34 | return schema.validate(params); 35 | } 36 | 37 | /** 38 | * @param {{ id: string }} body 39 | * @returns {Joi.ValidationResult<{ id: string }>} 40 | * @memberof UserValidation 41 | */ 42 | getUser( 43 | body: { 44 | id: string 45 | }, 46 | ): Joi.ValidationResult { 47 | const schema: Joi.Schema = Joi.object().keys({ 48 | id: this.customJoi.objectId().required(), 49 | }); 50 | 51 | return schema.validate(body); 52 | } 53 | 54 | /** 55 | * @param {{ id: string }} body 56 | * @returns {Joi.ValidationResult<{ id: string }>} 57 | * @memberof UserValidation 58 | */ 59 | removeUser( 60 | body: { 61 | id: string 62 | }, 63 | ): Joi.ValidationResult { 64 | const schema: Joi.Schema = Joi.object().keys({ 65 | id: this.customJoi.objectId().required(), 66 | }); 67 | 68 | return schema.validate(body); 69 | } 70 | } 71 | 72 | export default new UserValidation(); 73 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/index.ts: -------------------------------------------------------------------------------- 1 | import * as AuthComponent from './Auth'; 2 | import * as UserComponent from './User'; 3 | 4 | export { 5 | AuthComponent, 6 | UserComponent, 7 | }; 8 | -------------------------------------------------------------------------------- /generators/app/templates/src/components/validation.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import { Types } from 'mongoose'; 3 | 4 | /** 5 | * @export 6 | * @class Validation 7 | */ 8 | abstract class Validation { 9 | // can`t assign to customJoi any type of Joi Schemas - because of custom field objectId. Need to discuss this 10 | customJoi: any; 11 | 12 | /** 13 | * @static 14 | * @type {string} 15 | * @memberof JoiSchema 16 | */ 17 | readonly messageObjectId: string = 'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'; 18 | 19 | /** 20 | * Creates an instance of Schema. 21 | * @memberof JoiSchema 22 | */ 23 | constructor() { 24 | this.customJoi = Joi.extend((joi) => ({ 25 | type: 'objectId', 26 | base: joi.string(), 27 | validate( 28 | value: any, 29 | helpers: Joi.CustomHelpers, 30 | ): Object | string { 31 | if (!Types.ObjectId.isValid(value)) { 32 | return this.createError( 33 | 'objectId.base', 34 | { 35 | value, 36 | }, 37 | helpers, 38 | ); 39 | } 40 | 41 | return value; // Keep the value as it was 42 | }, 43 | })); 44 | } 45 | } 46 | 47 | export default Validation; 48 | -------------------------------------------------------------------------------- /generators/app/templates/src/config/connection/connection.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import config from '../env/index'; 3 | 4 | const MONGO_URI: string = `${config.database.MONGODB_URI}${config.database.MONGODB_DB_MAIN}`; 5 | 6 | export const db: mongoose.Connection = mongoose.createConnection(MONGO_URI); 7 | 8 | // handlers 9 | db.on('connecting', () => { 10 | console.log('\x1b[32m', 'MongoDB :: connecting'); 11 | }); 12 | 13 | db.on('error', (error) => { 14 | console.log('\x1b[31m', `MongoDB :: connection ${error}`); 15 | mongoose.disconnect(); 16 | }); 17 | 18 | db.on('connected', () => { 19 | console.log('\x1b[32m', 'MongoDB :: connected'); 20 | }); 21 | 22 | db.once('open', () => { 23 | console.log('\x1b[32m', 'MongoDB :: connection opened'); 24 | }); 25 | 26 | db.on('reconnected', () => { 27 | console.log('\x1b[33m"', 'MongoDB :: reconnected'); 28 | }); 29 | 30 | db.on('reconnectFailed', () => { 31 | console.log('\x1b[31m', 'MongoDB :: reconnectFailed'); 32 | }); 33 | 34 | db.on('disconnected', () => { 35 | console.log('\x1b[31m', 'MongoDB :: disconnected'); 36 | }); 37 | 38 | db.on('fullsetup', () => { 39 | console.log('\x1b[33m"', 'MongoDB :: reconnecting... %d'); 40 | }); 41 | -------------------------------------------------------------------------------- /generators/app/templates/src/config/env/index.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | interface IConfig { 6 | port: string | number; 7 | database: { 8 | MONGODB_URI: string; 9 | MONGODB_DB_MAIN: string; 10 | }; 11 | <%_ if(sessionStore === 'redis') { _%> 12 | redis: { 13 | port: number; 14 | host: string; 15 | }; 16 | <%_ }_%> 17 | secret: string; 18 | } 19 | 20 | const NODE_ENV: string = process.env.NODE_ENV || 'development'; 21 | 22 | const development: IConfig = { 23 | port: process.env.PORT || 3000, 24 | database: { 25 | MONGODB_URI: process.env.MONGODB_URI || 'mongodb://localhost:27017/', 26 | MONGODB_DB_MAIN: process.env.MONGODB_DB_MAIN || 'users_db', 27 | }, 28 | <%_ if(sessionStore === 'redis') { _%> 29 | redis: { 30 | port: parseInt(process.env.REDIS_PORT, 10) || 6379, 31 | host: process.env.REDIS_HOST || '127.0.0.1', 32 | }, 33 | <%_ }_%> 34 | secret: process.env.SECRET || '@QEGTUI', 35 | }; 36 | 37 | const production: IConfig = { 38 | port: process.env.PORT || 3000, 39 | database: { 40 | MONGODB_URI: process.env.MONGODB_URI || 'mongodb://production_uri/', 41 | MONGODB_DB_MAIN: process.env.MONGODB_DB_MAIN || 'users_db', 42 | }, 43 | <%_ if(sessionStore === 'redis') { _%> 44 | redis: { 45 | port: parseInt(process.env.REDIS_PORT, 10) || 6379, 46 | host: process.env.REDIS_HOST || '127.0.0.1', 47 | }, 48 | <%_ }_%> 49 | secret: process.env.SECRET || '@QEGTUI', 50 | }; 51 | 52 | const test: IConfig = { 53 | port: process.env.PORT || 3000, 54 | database: { 55 | MONGODB_URI: process.env.MONGODB_URI || 'mongodb://localhost:27017', 56 | MONGODB_DB_MAIN: 'test_users_db', 57 | }, 58 | <%_ if(sessionStore === 'redis') { _%> 59 | redis: { 60 | port: parseInt(process.env.REDIS_PORT, 10) || 6379, 61 | host: process.env.REDIS_HOST || '127.0.0.1', 62 | }, 63 | <%_ }_%> 64 | secret: process.env.SECRET || '@QEGTUI', 65 | }; 66 | 67 | const config: { 68 | [name: string]: IConfig 69 | } = { 70 | test, 71 | development, 72 | production, 73 | }; 74 | 75 | export default config[NODE_ENV]; 76 | -------------------------------------------------------------------------------- /generators/app/templates/src/config/error/index.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | 3 | /** 4 | * @export 5 | * @class HttpError 6 | * @extends {Error} 7 | */ 8 | export class HttpError extends Error { 9 | status: number; 10 | 11 | message: string; 12 | 13 | name: 'HttpError'; 14 | 15 | /** 16 | * Creates an instance of HttpError. 17 | * @param {number} [status] 18 | * @param {string} [message] 19 | * @memberof HttpError 20 | */ 21 | constructor(status ? : number, message ? : string) { 22 | super(message); 23 | 24 | Error.captureStackTrace(this, this.constructor); 25 | 26 | this.status = status || 500; 27 | this.name = this.name || 'HttpError'; 28 | this.message = message || http.STATUS_CODES[this.status] || 'Error'; 29 | } 30 | } 31 | 32 | export default HttpError; 33 | -------------------------------------------------------------------------------- /generators/app/templates/src/config/error/sendHttpError.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request } from 'express'; 2 | import * as express from 'express'; 3 | import { HttpError } from './index'; 4 | 5 | interface CustomResponse extends express.Response { 6 | sendHttpError: (error: HttpError | Error, message ? : string) => void; 7 | } 8 | 9 | /** 10 | * 11 | * @param error Error 12 | * @returns {string} HTML response or empty string 13 | * @description generates HTML for response 14 | */ 15 | const generateHTML: Function = (error: HttpError): string => { 16 | if (error) { 17 | return '
' 18 | + `

Status: ${error.status}

` 19 | + `

Name: ${error.name}

` 20 | + `

${error}

` 21 | + '
'; 22 | } 23 | 24 | return ''; 25 | }; 26 | 27 | /** 28 | * @exports 29 | * @param {Request} req 30 | * @param {*} res 31 | * @param {NextFunction} next 32 | * 33 | * @swagger 34 | * components: 35 | * schemas: 36 | * Error: 37 | * type: object 38 | * required: 39 | * - status 40 | * - message 41 | * properties: 42 | * status: 43 | * type: integer 44 | * description: HTTP status code 45 | * example: 200 46 | * message: 47 | * type: string 48 | * description: Error description 49 | * example: User created 50 | */ 51 | export function sendHttpErrorModule(req: Request, res: CustomResponse, next: NextFunction): void { 52 | res.sendHttpError = (error: HttpError): void => { 53 | res.status(error.status); 54 | 55 | /** 56 | * if this looks like an AJAX request 57 | * if this request has a "json" content-type AND ALSO has its "Accept" header set 58 | * if this request DOESN'T explicitly want HTML 59 | */ 60 | if ( 61 | req.xhr 62 | || req.is('json') 63 | || (req.is('json') && req.get('Accept')) 64 | || !(req.get('Accept') && req.get('Accept').indexOf('html') !== -1) 65 | ) { 66 | res.json({ 67 | status: error.status, 68 | name: error.name, 69 | message: error.message, 70 | }); 71 | } else { 72 | res.send(generateHTML(error)); 73 | } 74 | }; 75 | 76 | next(); 77 | } 78 | -------------------------------------------------------------------------------- /generators/app/templates/src/config/middleware/middleware.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as compression from 'compression'; 3 | import * as cookieParser from 'cookie-parser'; 4 | import * as cors from 'cors'; 5 | import * as express from 'express'; 6 | import * as helmet from 'helmet'; 7 | <%_ if(authentication === 'passport-local-strategy') { _%> 8 | import * as passport from 'passport'; 9 | import * as session from 'express-session'; 10 | import config from '../env/index'; 11 | <%_ }_%> 12 | <%_ if(sessionStore === 'mongo') { _%> 13 | import MongoStore from 'connect-mongo'; 14 | <%_ }_%> 15 | <%_ if(sessionStore === 'redis') { _%> 16 | import Redis from 'ioredis'; 17 | import * as connectRedis from 'connect-redis'; 18 | <%_ }_%> 19 | import { HttpError } from '../error/index'; 20 | import { sendHttpErrorModule } from '../error/sendHttpError'; 21 | <%_ if(sessionStore === 'redis') { _%> 22 | 23 | const RedisStore: connectRedis.RedisStore = connectRedis(session); 24 | <%_ }_%> 25 | 26 | /** 27 | * @export 28 | * @param {express.Application} app 29 | */ 30 | export function configure(app: express.Application): void { 31 | // express middleware 32 | app.use(bodyParser.urlencoded({ 33 | extended: false, 34 | })); 35 | app.use(bodyParser.json()); 36 | // parse Cookie header and populate req.cookies with an object keyed by the cookie names. 37 | app.use(cookieParser()); 38 | // returns the compression middleware 39 | app.use(compression()); 40 | // helps you secure your Express apps by setting various HTTP headers 41 | app.use(helmet()); 42 | // providing a Connect/Express middleware that can be used to enable CORS with various options 43 | app.use(cors()); 44 | 45 | <%_ if(authentication === 'passport-local-strategy') { _%> 46 | /** 47 | * @swagger 48 | * components: 49 | * securitySchemes: 50 | * cookieAuth: 51 | * type: apiKey 52 | * in: cookie 53 | * name: sid 54 | */ 55 | app.use(session({ 56 | resave: true, 57 | saveUninitialized: true, 58 | secret: config.secret, 59 | name: 'api.sid', 60 | <%_ if(sessionStore === 'mongo') { _%> 61 | store: process.env.NODE_ENV === 'development' 62 | ? new session.MemoryStore() 63 | : MongoStore.create({ 64 | mongoUrl: `${config.database.MONGODB_URI}${config.database.MONGODB_DB_MAIN}`, 65 | }), 66 | <%_ }_%> 67 | <%_ if(sessionStore === 'redis') { _%> 68 | store: new RedisStore({ 69 | client: new Redis({ 70 | port: config.redis.port, 71 | host: config.redis.host, 72 | }), 73 | }), 74 | <%_ }_%> 75 | })); 76 | app.use(passport.initialize()); 77 | app.use(passport.session()); 78 | <%_ }_%> 79 | // custom errors 80 | app.use(sendHttpErrorModule); 81 | 82 | // cors 83 | app.use((req, res, next) => { 84 | res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS '); 85 | res.header( 86 | 'Access-Control-Allow-Headers', 87 | 'Origin, X-Requested-With,' 88 | + ' Content-Type, Accept,' 89 | + ' Authorization,' 90 | + ' Access-Control-Allow-Credentials', 91 | ); 92 | res.header('Access-Control-Allow-Credentials', 'true'); 93 | next(); 94 | }); 95 | } 96 | 97 | interface CustomResponse extends express.Response { 98 | sendHttpError: (error: HttpError | Error, message ? : string) => void; 99 | } 100 | 101 | /** 102 | * @export 103 | * @param {express.Application} app 104 | */ 105 | export function initErrorHandler(app: express.Application): void { 106 | app.use((error: Error, req: express.Request, res: CustomResponse) => { 107 | if (typeof error === 'number') { 108 | error = new HttpError(error); // next(404) 109 | } 110 | 111 | if (error instanceof HttpError) { 112 | res.sendHttpError(error); 113 | } else if (app.get('env') === 'development') { 114 | error = new HttpError(500, error.message); 115 | res.sendHttpError(error); 116 | } else { 117 | error = new HttpError(500); 118 | res.sendHttpError(error, error.message); 119 | } 120 | 121 | console.error(error); 122 | }); 123 | } 124 | -------------------------------------------------------------------------------- /generators/app/templates/src/config/server/index.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as serverHandlers from './serverHandlers'; 3 | import server from './server'; 4 | 5 | const Server: http.Server = http.createServer(server); 6 | 7 | /** 8 | * Binds and listens for connections on the specified host 9 | */ 10 | Server.listen(server.get('port')); 11 | 12 | /** 13 | * Server Events 14 | */ 15 | Server.on('error', (error: Error) => serverHandlers.onError(error, server.get('port'))); 16 | Server.on('listening', serverHandlers.onListening.bind(Server)); 17 | -------------------------------------------------------------------------------- /generators/app/templates/src/config/server/server.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as Middleware from '../middleware/middleware'; 3 | import * as Routes from '../../routes'; 4 | 5 | /** 6 | * @constant {express.Application} 7 | */ 8 | const app: express.Application = express(); 9 | 10 | /** 11 | * @constructs express.Application Middleware 12 | */ 13 | Middleware.configure(app); 14 | 15 | /** 16 | * @constructs express.Application Routes 17 | */ 18 | Routes.init(app); 19 | 20 | /** 21 | * @constructs express.Application Error Handler 22 | */ 23 | Middleware.initErrorHandler(app); 24 | 25 | /** 26 | * sets port 3000 to default or unless otherwise specified in the environment 27 | */ 28 | app.set('port', process.env.PORT || 3000); 29 | 30 | /** 31 | * sets secret to 'superSecret', otherwise specified in the environment 32 | */ 33 | app.set('secret', process.env.SECRET || 'superSecret'); 34 | 35 | /** 36 | * @exports {express.Application} 37 | */ 38 | export default app; 39 | -------------------------------------------------------------------------------- /generators/app/templates/src/config/server/serverHandlers.ts: -------------------------------------------------------------------------------- 1 | import * as debug from 'debug'; 2 | import { Address } from 'cluster'; 3 | 4 | /** 5 | * @param {NodeJS.ErrnoException} error 6 | * @param {number|string|boolean} port 7 | * @returns throw error 8 | */ 9 | // eslint-disable-next-line no-undef 10 | export function onError(error: NodeJS.ErrnoException, port: number | string | boolean): void { 11 | if (error.syscall !== 'listen') { 12 | throw error; 13 | } 14 | 15 | const bind: string = (typeof port === 'string') ? `Pipe ${port}` : `Port ${port}`; 16 | 17 | switch (error.code) { 18 | case 'EACCES': 19 | console.error(`${bind} requires elevated privileges`); 20 | process.exit(1); 21 | 22 | break; 23 | case 'EADDRINUSE': 24 | console.error(`${bind} is already in use`); 25 | process.exit(1); 26 | 27 | break; 28 | default: 29 | throw error; 30 | } 31 | } 32 | 33 | /** 34 | * @export onListening 35 | */ 36 | export function onListening(): void { 37 | const addr: Address = this.address(); 38 | const bind: string = (typeof addr === 'string') ? `pipe ${addr}` : `port ${addr.port}`; 39 | 40 | debug(`Listening on ${bind}`); 41 | } 42 | -------------------------------------------------------------------------------- /generators/app/templates/src/routes/AuthRouter.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { AuthComponent } from '../components'; 3 | 4 | /** 5 | * @constant {express.Router} 6 | */ 7 | const router: Router = Router(); 8 | 9 | /** 10 | * POST method route 11 | * @example http://localhost:PORT/signup 12 | * @swagger 13 | * /auth/signup/: 14 | * post: 15 | * description: sign up user to application 16 | * tags: ["auth"] 17 | * requestBody: 18 | * description: sign up body 19 | * required: true 20 | * content: 21 | * application/json: 22 | * schema: 23 | * $ref: '#/components/schemas/UserSchema' 24 | * example: 25 | * email: test.user@mail.com 26 | * password: test_test 27 | * responses: 28 | * 200: 29 | * description: user successfuly signed in 30 | * content: 31 | * appication/json: 32 | * example: 33 | * status: 200 34 | * logged: true 35 | * message: Sign in successfull!! 36 | * 400: 37 | * description: sign in failed 38 | * content: 39 | * application/json: 40 | * example: 41 | * status: 400 42 | * logged: false 43 | * message: Email already exists 44 | */ 45 | router.post('/signup', AuthComponent.signup); 46 | 47 | /** 48 | * POST method route 49 | * @example http://localhost:PORT/login 50 | * 51 | * @swagger 52 | * /auth/login/: 53 | * post: 54 | * description: Login user to application 55 | * tags: ["auth"] 56 | * requestBody: 57 | * description: login body 58 | * required: true 59 | * content: 60 | * application/json: 61 | * schema: 62 | * $ref: '#/components/schemas/UserSchema' 63 | * example: 64 | * email: test.user@mail.com 65 | * password: test_test 66 | * responses: 67 | * 200: 68 | * description: user successfuly logged 69 | * content: 70 | * appication/json: 71 | * example: 72 | * status: 200 73 | * logged: true 74 | * message: Successfully logged! 75 | * 401: 76 | * description: Not logged, invalid credentials 77 | * content: 78 | * application/json: 79 | * example: 80 | * status: 401 81 | * logged: false 82 | * message: Invalid credentials 83 | */ 84 | router.post('/login', AuthComponent.login); 85 | <%_ if(authentication === 'passport-local-strategy') { _%> 86 | /** 87 | * POST method route 88 | * @example http://localhost:3000 89 | * 90 | * @swagger 91 | * /auth/logout/: 92 | * post: 93 | * description: Loogout from application 94 | * tags: ["auth"] 95 | * responses: 96 | * 200: 97 | * description: users successfuly logout 98 | * content: 99 | * application/json: 100 | * example: 101 | * status: 200 102 | * logged: false 103 | * message: Successfuly logged out 104 | * 401: 105 | * description: cant logout user, because he didnt login to app 106 | * content: 107 | * application/json: 108 | * example: 109 | * status: 401 110 | * logged: false 111 | * message: You are not authorized to app. Can't logout 112 | */ 113 | router.post('/logout', AuthComponent.logout); 114 | <%_ }_%> 115 | 116 | <%_ if(authentication === 'oauth2.0') { _%> 117 | /** 118 | * POST method route 119 | * @example http://localhost:PORT/token 120 | */ 121 | router.post('/token', AuthComponent.token); 122 | 123 | <%_ }_%> 124 | /** 125 | * @export {express.Router} 126 | */ 127 | export default router; 128 | -------------------------------------------------------------------------------- /generators/app/templates/src/routes/UserRouter.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { UserComponent } from '../components'; 3 | 4 | /** 5 | * @constant {express.Router} 6 | */ 7 | const router: Router = Router(); 8 | 9 | /** 10 | * GET method route 11 | * @example http://localhost:PORT/v1/users 12 | * 13 | * @swagger 14 | * /v1/users: 15 | * get: 16 | * description: Get all stored users in Database 17 | * tags: ["users"] 18 | <%_ if(authentication === 'passport-local-strategy') { _%> 19 | * security: 20 | * - cookieAuth: [] 21 | <%_ }_%> 22 | <%_ if(authentication === 'jwt-auth') { _%> 23 | * security: 24 | * - ApiKeyAuth: [] 25 | <%_ }_%> 26 | * responses: 27 | * 200: 28 | * description: An array of users 29 | * content: 30 | * application/json: 31 | * schema: 32 | * oneOf: 33 | * - $ref: '#/components/schemas/Users' 34 | * default: 35 | * description: unexpected error 36 | * content: 37 | * application/json: 38 | * schema: 39 | * $ref: '#/components/schemas/Error' 40 | */ 41 | router.get('/', UserComponent.findAll); 42 | 43 | /** 44 | * POST method route 45 | * @example http://localhost:PORT/v1/users 46 | * 47 | * @swagger 48 | * /v1/users: 49 | * post: 50 | * description: Create new User 51 | * tags: ["users"] 52 | <%_ if(authentication === 'passport-local-strategy') { _%> 53 | * security: 54 | * - cookieAuth: [] 55 | <%_ }_%> 56 | <%_ if(authentication === 'jwt-auth') { _%> 57 | * security: 58 | * - ApiKeyAuth: [] 59 | <%_ }_%> 60 | * requestBody: 61 | * description: user creation request body 62 | * required: true 63 | * content: 64 | * application/json: 65 | * schema: 66 | * $ref: '#/components/schemas/UserSchema' 67 | * example: 68 | * name: userName 69 | * email: test.user@mail.com 70 | * responses: 71 | * 201: 72 | * description: return created user 73 | * content: 74 | * application/json: 75 | * schema: 76 | * oneOf: 77 | * - $ref: '#/components/schemas/UserSchema' 78 | * default: 79 | * description: unexpected error 80 | * content: 81 | * application/json: 82 | * schema: 83 | * $ref: '#/components/schemas/Error' 84 | */ 85 | router.post('/', UserComponent.create); 86 | 87 | /** 88 | * GET method route 89 | * @example http://localhost:PORT/v1/users/:id 90 | * 91 | * @swagger 92 | * /v1/users/{id}: 93 | * get: 94 | * description: Get user by userId 95 | * tags: ["users"] 96 | <%_ if(authentication === 'passport-local-strategy') { _%> 97 | * security: 98 | * - cookieAuth: [] 99 | <%_ }_%> 100 | <%_ if(authentication === 'jwt-auth') { _%> 101 | * security: 102 | * - ApiKeyAuth: [] 103 | <%_ }_%> 104 | * parameters: 105 | * - in: path 106 | * name: id 107 | * description: the unique userId 108 | * required: true 109 | * schema: 110 | * type: string 111 | * responses: 112 | * 200: 113 | * description: return user by id 114 | * content: 115 | * application/json: 116 | * schema: 117 | * oneOf: 118 | * - $ref: '#/components/schemas/UserSchema' 119 | */ 120 | router.get('/:id', UserComponent.findOne); 121 | 122 | /** 123 | * DELETE method route 124 | * @example http://localhost:PORT/v1/users/:id 125 | * 126 | * @swagger 127 | * /v1/users/{id}: 128 | * delete: 129 | * description: Delete user by userId 130 | * tags: ["users"] 131 | <%_ if(authentication === 'passport-local-strategy') { _%> 132 | * security: 133 | * - cookieAuth: [] 134 | <%_ }_%> 135 | <%_ if(authentication === 'jwt-auth') { _%> 136 | * security: 137 | * - ApiKeyAuth: [] 138 | <%_ }_%> 139 | * parameters: 140 | * - in: path 141 | * name: id 142 | * description: the unique userId 143 | * required: true 144 | * schema: 145 | * type: string 146 | * responses: 147 | * 200: 148 | * description: return deleted user 149 | * content: 150 | * application/json: 151 | * schema: 152 | * oneOf: 153 | * - $ref: '#/components/schemas/UserSchema' 154 | */ 155 | router.delete('/:id', UserComponent.remove); 156 | 157 | /** 158 | * @export {express.Router} 159 | */ 160 | export default router; 161 | -------------------------------------------------------------------------------- /generators/app/templates/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as http from 'http'; 3 | import * as path from 'path'; 4 | import * as swaggerJSDoc from 'swagger-jsdoc'; 5 | import * as swaggerUi from 'swagger-ui-express'; 6 | <%_ if(authentication === 'passport-local-strategy') { _%> 7 | import * as passportConfig from '../config/middleware/passport'; 8 | <%_ }_%> 9 | <%_ if(authentication === 'jwt-auth') { _%> 10 | import * as jwtConfig from '../config/middleware/jwtAuth'; 11 | <%_ }_%> 12 | import AuthRouter from './AuthRouter'; 13 | import UserRouter from './UserRouter'; 14 | <%_ if(authentication === 'oauth2.0') { _%> 15 | import authenticate from '../config/middleware/oAuth'; 16 | <%_ }_%> 17 | 18 | const swaggerDef = require('../../swaggerDef'); 19 | 20 | /** 21 | * @export 22 | * @param {express.Application} app 23 | */ 24 | export function init(app: express.Application): void { 25 | const router: express.Router = express.Router(); 26 | 27 | /** 28 | * @description 29 | * Forwards any requests to the /v1/users URI to our UserRouter 30 | * Also, check if user authenticated 31 | * @constructs 32 | */ 33 | <%_ if(authentication === 'passport-local-strategy') { _%> 34 | app.use('/v1/users', passportConfig.isAuthenticated, UserRouter); 35 | <%_ }_%> 36 | <%_ if(authentication === 'jwt-auth') { _%> 37 | app.use('/v1/users', jwtConfig.isAuthenticated, UserRouter); 38 | <%_ }_%> 39 | <%_ if(authentication === 'oauth2.0') { _%> 40 | app.use('/v1/users', authenticate(), UserRouter); 41 | <%_ }_%> 42 | 43 | /** 44 | * @description Forwards any requests to the /auth URI to our AuthRouter 45 | * @constructs 46 | */ 47 | app.use('/auth', AuthRouter); 48 | 49 | /** 50 | * @description 51 | * If swagger.json file exists in root folder, shows swagger api description 52 | * else send commands, how to get swagger.json file 53 | * @constructs 54 | */ 55 | app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerJSDoc({ 56 | swaggerDefinition: swaggerDef, 57 | apis: [path.join(__dirname, '../../src/**/**/*.ts')], 58 | }))); 59 | 60 | /** 61 | * @description No results returned mean the object is not found 62 | * @constructs 63 | */ 64 | app.use((req, res) => { 65 | res.status(404).send(http.STATUS_CODES[404]); 66 | }); 67 | 68 | /** 69 | * @constructs all routes 70 | */ 71 | app.use(router); 72 | } 73 | -------------------------------------------------------------------------------- /generators/app/templates/test/api.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const request = require('supertest'); 3 | const app = require('../src/config/server/server').default; 4 | const UserModel = require('../src/components/User/model').default; 5 | <%_ if(authentication === 'oauth2.0') { _%> 6 | const ClientModel = require('../src/config/oauth/clientModel').default; 7 | const TokenModel = require('../src/config/oauth/tokenModel').default; 8 | const AuthCodeModel = require('../src/config/oauth/authCodeModel').default; 9 | <%_ }_%> 10 | chai.should(); 11 | 12 | /** 13 | * API tests 14 | */ 15 | describe('API', () => { 16 | it('get all users', (done) => { 17 | request(app) 18 | .get('/v1/users') 19 | <%_ if(authentication === 'oauth2.0') { _%> 20 | .set('Authorization', `Bearer ${global.accessToken}`) 21 | <%_ }_%> 22 | <%_ if(authentication === 'passport-local-strategy') { _%> 23 | .set('Cookie', global.cookie) 24 | <%_ }_%> 25 | <%_ if(authentication === 'jwt-auth') { _%> 26 | .set('x-access-token', global.token) 27 | <%_ }_%> 28 | .expect((res) => { 29 | res.status.should.equal(200); 30 | res.body.should.be.an('array'); 31 | }) 32 | .end(done); 33 | }); 34 | 35 | it('create new user', (done) => { 36 | const newUser = { 37 | email: 'new.user@gmail.com', 38 | name: 'John Doe' 39 | }; 40 | 41 | request(app) 42 | .post('/v1/users') 43 | .send(newUser) 44 | <%_ if(authentication === 'oauth2.0') { _%> 45 | .set('Authorization', `Bearer ${global.accessToken}`) 46 | <%_ }_%> 47 | <%_ if(authentication === 'passport-local-strategy') { _%> 48 | .set('Cookie', global.cookie) 49 | <%_ }_%> 50 | <%_ if(authentication === 'jwt-auth') { _%> 51 | .set('x-access-token', global.token) 52 | <%_ }_%> 53 | .expect((res) => { 54 | res.status.should.equal(201); 55 | res.body.should.have.property('email'); 56 | }) 57 | .end(done); 58 | }); 59 | }); 60 | 61 | /** 62 | * clear database after tests 63 | */ 64 | after(async () => { 65 | try { 66 | <%_ if(authentication === 'oauth2.0') { _%> 67 | 68 | await AuthCodeModel.collection.drop(); 69 | await TokenModel.collection.drop(); 70 | await ClientModel.collection.drop(); 71 | <%_ }_%> 72 | await UserModel.collection.drop(); 73 | } catch (error) { 74 | console.log('Something went wrong after tests, seems your database doesnt cleaned'); 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /generators/app/templates/test/authentication.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const request = require('supertest'); 3 | const app = require('../src/config/server/server').default; 4 | const user = require('./fixtures/user.json'); 5 | <%_ if(authentication === 'oauth2.0') { _%> 6 | const q = require('querystring'); 7 | const ClientModel = require('../src/config/oauth/clientModel').default; 8 | const TokenModel = require('../src/config/oauth/tokenModel').default; 9 | const OAuthClient = require('./fixtures/OAuthClient.json'); 10 | const OAuthToken = require('./fixtures/OAuthToken'); 11 | const UserModel = require('../src/components/User/model').default; 12 | <%_ }_%> 13 | chai.should(); 14 | 15 | /** 16 | * storing globals to access them in API requests 17 | */ 18 | <%_ if(authentication === 'jwt-auth') { _%> 19 | global.token = ''; 20 | <%_ }_%> 21 | <%_ if(authentication === 'passport-local-strategy') { _%> 22 | global.cookie = ''; 23 | <%_ }_%> 24 | <%_ if(authentication === 'oauth2.0') { _%> 25 | global.code = ''; 26 | global.accessToken = ''; 27 | 28 | /** 29 | * Creating default client 30 | */ 31 | before(async () => { 32 | try { 33 | await ClientModel.findOneAndUpdate({ 34 | id: OAuthClient.id 35 | }, 36 | OAuthClient, { 37 | upsert: true, 38 | new: true 39 | } 40 | ); 41 | 42 | } catch (error) { 43 | throw new Error(error); 44 | } 45 | }); 46 | <%_ }_%> 47 | /** 48 | * Authentication tests 49 | */ 50 | describe('Authentication', () => { 51 | <%_ if(authentication === 'oauth2.0') { _%> 52 | it('sign up', () => { 53 | return request(app) 54 | .post('/auth/signup') 55 | .send(user) 56 | .expect('Content-type', /json/) 57 | .then(async (res) => { 58 | res.body.status.should.equal(200); 59 | res.body.user.should.have.property('email'); 60 | 61 | const user = await UserModel.findOne({ 62 | email: res.body.user.email 63 | }); 64 | 65 | OAuthToken.user = user._id.toString(); 66 | 67 | await TokenModel.findOneAndUpdate({ 68 | user: OAuthToken.user 69 | }, 70 | OAuthToken, { 71 | upsert: true 72 | } 73 | ); 74 | 75 | }); 76 | }); 77 | <%_ }_%> 78 | <%_ if(authentication === 'passport-local-strategy' || authentication === 'jwt-auth') { _%> 79 | it('sign up', (done) => { 80 | request(app) 81 | .post('/auth/signup') 82 | .send(user) 83 | .expect('Content-type', /json/) 84 | .expect((res) => { 85 | res.body.status.should.equal(200); 86 | res.body.logged.should.equal(true); 87 | res.body.message.should.be.a('string'); 88 | <%_ if(authentication === 'passport-local-strategy') { _%> 89 | global.cookie = res.header['set-cookie']; 90 | <%_ }_%> 91 | <%_ if(authentication === 'jwt-auth') { _%> 92 | global.token = res.body.token; 93 | <%_ }_%> 94 | }) 95 | .end(done) 96 | }); 97 | <%_ }_%> 98 | it('sign up user with existing email', (done) => { 99 | request(app) 100 | .post('/auth/signup') 101 | .send(user) 102 | .expect('Content-type', /json/) 103 | .expect((res) => { 104 | res.body.status.should.equal(400); 105 | }) 106 | .end(done); 107 | }); 108 | <%_ if(authentication === 'oauth2.0') { _%> 109 | it('login to app, redirects to your redirectUri with code', (done) => { 110 | request(app) 111 | .post('/auth/login?' + 112 | `client_id=${OAuthClient.id}&` + 113 | 'state=randomstring&' + 114 | 'response_type=code' 115 | ) 116 | .send(user) 117 | .expect((res) => { 118 | res.status.should.equal(302); 119 | global.code = q.parse(res.header['location'].split('?')[1]).code; 120 | }) 121 | .end(done); 122 | }); 123 | <%_ }_%> 124 | <%_ if(authentication === 'passport-local-strategy' || authentication === 'jwt-auth') { _%> 125 | it('login to app', (done) => { 126 | request(app) 127 | .post('/auth/login') 128 | .send(user) 129 | .expect('Content-type', /json/) 130 | .expect((res) => { 131 | res.body.status.should.equal(200); 132 | res.body.logged.should.equal(true); 133 | res.body.message.should.be.a('string'); 134 | <%_ if(authentication === 'passport-local-strategy') { _%> 135 | global.cookie = res.header['set-cookie']; 136 | <%_ }_%> 137 | <%_ if(authentication === 'jwt-auth') { _%> 138 | global.token = res.body.token; 139 | <%_ }_%> 140 | }) 141 | .end(done); 142 | }); 143 | <%_ }_%> 144 | <%_ if(authentication === 'oauth2.0') { _%> 145 | it('getting tokens', (done) => { 146 | const data = { 147 | code: global.code, 148 | client_id: OAuthClient.id, 149 | client_secret: OAuthClient.secret, 150 | grant_type: 'authorization_code' 151 | }; 152 | 153 | request(app) 154 | .post(`/auth/token?redirect_uri=${OAuthClient.redirectUris[0]}`) 155 | .set('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8') 156 | .send(data) 157 | .expect((res) => { 158 | res.status.should.equal(200); 159 | res.body.should.have.property('accessToken'); 160 | res.body.should.have.property('refreshToken'); 161 | global.accessToken = res.body.accessToken; 162 | }) 163 | .end(done); 164 | }); 165 | <%_ }_%> 166 | }); 167 | -------------------------------------------------------------------------------- /generators/app/templates/test/fixtures/OAuthClient.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "yourClientApplicationId", 3 | "secret": "yourClientSecret", 4 | "grants": [ 5 | "authorization_code", 6 | "refresh_token" 7 | ], 8 | "redirectUris": [ 9 | "http://yourRedirectUri" 10 | ], 11 | "type": "confidential" 12 | } 13 | -------------------------------------------------------------------------------- /generators/app/templates/test/fixtures/OAuthToken.js: -------------------------------------------------------------------------------- 1 | const date = new Date(); 2 | 3 | module.exports = { 4 | accessToken: 'accessToken', 5 | accessTokenExpiresAt: date.setSeconds(date.getSeconds() + 60 * 60), 6 | refreshToken: 'refreshToken', 7 | refreshTokenExpiresAt: date.setSeconds(date.getSeconds() + 60 * 60 * 24 * 24), 8 | client: { 9 | id: 'yourClientApplicationId', 10 | grants: [ 11 | 'authorization_code', 12 | 'refresh_token' 13 | ] 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /generators/app/templates/test/fixtures/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "test@mail.com", 3 | "password": "test123" 4 | } 5 | -------------------------------------------------------------------------------- /generators/app/templates/test/index.js: -------------------------------------------------------------------------------- 1 | require('./authentication'); 2 | require('./api'); 3 | -------------------------------------------------------------------------------- /generators/authentication/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Generator = require('yeoman-generator'); 3 | 4 | const authGenerator = class extends Generator { 5 | writing() { 6 | const { 7 | name, 8 | authentication, 9 | clientId, 10 | clientSecret, 11 | redirectUris 12 | } = this.options; 13 | const templates = { 14 | authentication: authentication === 'jwt-auth' ? 'jwtAuth.ts' : (authentication === 'oauth2.0' ? 'oAuth.ts' : 'passport.ts') 15 | }; 16 | this.fs.copyTpl( 17 | this.templatePath(`src/**/**/${templates.authentication}`), 18 | this.destinationPath(name + '/src/'), { 19 | name: name 20 | } 21 | ); 22 | 23 | if (authentication === 'oauth2.0') { 24 | this.fs.copyTpl( 25 | this.templatePath(`src/config/oauth`), 26 | this.destinationPath(name + '/src/config/oauth'), { 27 | name, 28 | clientId, 29 | clientSecret, 30 | redirectUris 31 | } 32 | ) 33 | } 34 | } 35 | } 36 | 37 | module.exports = authGenerator; 38 | -------------------------------------------------------------------------------- /generators/authentication/templates/src/config/middleware/jwtAuth.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import * as http from 'http'; 4 | import app from '../server/server'; 5 | import HttpError from '../error'; 6 | 7 | interface RequestWithUser extends Request { 8 | user: object | string; 9 | } 10 | 11 | /** 12 | * 13 | * @param {RequestWithUser} req 14 | * @param {Response} res 15 | * @param {NextFunction} next 16 | * @returns {void} 17 | * @swagger 18 | * components: 19 | * securitySchemes: 20 | * ApiKeyAuth: 21 | * type: apiKey 22 | * in: header 23 | * name: x-access-token 24 | */ 25 | export function isAuthenticated(req: RequestWithUser, res: Response, next: NextFunction): void { 26 | const token: string | string[] = req.headers['x-access-token']; 27 | 28 | if (token) { 29 | try { 30 | const user: object | string = jwt.verify(token.toString(), app.get('secret')); 31 | 32 | req.user = user; 33 | 34 | return next(); 35 | } catch (error) { 36 | return next(new HttpError(401, http.STATUS_CODES[401])); 37 | } 38 | } 39 | 40 | return next(new HttpError(400, 'No token provided')); 41 | } 42 | -------------------------------------------------------------------------------- /generators/authentication/templates/src/config/middleware/oAuth.ts: -------------------------------------------------------------------------------- 1 | import * as OAuth2Server from 'oauth2-server'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import HttpError from '../error'; 4 | import oauth from '../oauth'; 5 | 6 | type Opt = { 7 | scope ? : string | string[] 8 | }; 9 | /** 10 | * 11 | * @param {Opt} opt 12 | * @returns {Promise < void >} 13 | */ 14 | export default function (opt: Opt = {}) { 15 | return async function (req: Request, res: Response, next: NextFunction): Promise < void > { 16 | const reqOAuth: OAuth2Server.Request = new OAuth2Server.Request({ 17 | headers: { 18 | authorization: req.headers.authorization, 19 | }, 20 | method: req.method, 21 | query: req.query, 22 | body: req.body, 23 | }); 24 | 25 | const resOAuth: OAuth2Server.Response = new OAuth2Server.Response(res); 26 | try { 27 | await oauth.authenticate(reqOAuth, resOAuth, opt); 28 | 29 | return next(); 30 | } catch (error) { 31 | return next(new HttpError(error.status, error.message)); 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /generators/authentication/templates/src/config/middleware/passport.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as passport from 'passport'; 3 | import * as passportLocal from 'passport-local'; 4 | import { NextFunction, Request, Response } from 'express'; 5 | import HttpError from '../error'; 6 | import UserModel, { IUserModel } from '../../components/User/model'; 7 | 8 | type LocalStrategyType = typeof passportLocal.Strategy; 9 | 10 | const LocalStrategy: LocalStrategyType = passportLocal.Strategy; 11 | 12 | /** 13 | * @description 14 | * determines, which data of the user object should be stored in the session. 15 | * The result of the serializeUser method is attached to the session 16 | * as req.session.passport.user = {} 17 | */ 18 | passport.serializeUser((user: { 19 | id: number 20 | }, done: Function) => { 21 | done(undefined, user.id); 22 | }); 23 | 24 | /** 25 | * @description 26 | * checks if user exists in database 27 | * if everything ok, proceed to route 28 | */ 29 | passport.deserializeUser(async (id: number, done: Function) => { 30 | try { 31 | const user: IUserModel = await UserModel.findById(id); 32 | done(null, user); 33 | } catch (error) { 34 | done(error); 35 | } 36 | }); 37 | 38 | /** 39 | * @description 40 | * configuring new local strategy 41 | * and use it in passport 42 | */ 43 | passport.use(new LocalStrategy({ 44 | usernameField: 'email', 45 | }, async (email: string, password: string, done: Function): Promise < void > => { 46 | try { 47 | const user: IUserModel = await UserModel.findOne({ 48 | email: email.toLowerCase(), 49 | }); 50 | 51 | if (!user) { 52 | return done(undefined, false, { 53 | message: `Email ${email} not found.`, 54 | }); 55 | } 56 | 57 | const isMatched: boolean = await user.comparePassword(password); 58 | 59 | if (isMatched) { 60 | return done(undefined, user); 61 | } 62 | 63 | return done(undefined, false, { 64 | message: 'Invalid email or password.', 65 | }); 66 | } catch (error) { 67 | done(error); 68 | } 69 | })); 70 | 71 | /** 72 | * @description Login Required middleware. 73 | */ 74 | export function isAuthenticated(req: Request, res: Response, next: NextFunction): void { 75 | if (req.isAuthenticated()) { 76 | return next(); 77 | } 78 | 79 | next(new HttpError(401, http.STATUS_CODES[401])); 80 | } 81 | -------------------------------------------------------------------------------- /generators/authentication/templates/src/config/oauth/authCodeModel.ts: -------------------------------------------------------------------------------- 1 | import { Document, Schema } from 'mongoose'; 2 | import * as OAuth2Server from 'oauth2-server'; 3 | import * as connections from '../connection/connection'; 4 | /** 5 | * @export 6 | * @interface IAuthCodeModel 7 | * @extends {Document} 8 | */ 9 | export interface IAuthCodeModel extends Document, OAuth2Server.AuthorizationCodeModel { 10 | authorizationCode: string; 11 | expiresAt: Date; 12 | redirectUri: string; 13 | scope: string; 14 | client: OAuth2Server.Client; 15 | user: OAuth2Server.User; 16 | } 17 | 18 | const AuthCodeSchema: Schema = new Schema({ 19 | authorizationCode: { 20 | type: String, 21 | required: true, 22 | }, 23 | expiresAt: { 24 | type: Date, 25 | required: true, 26 | }, 27 | redirectUri: { 28 | type: String, 29 | required: true, 30 | }, 31 | scope: { 32 | type: String, 33 | }, 34 | client: { 35 | type: Object, 36 | required: true, 37 | }, 38 | user: { 39 | type: Object, 40 | required: true, 41 | }, 42 | }); 43 | 44 | export default connections.db.model< IAuthCodeModel >('AuthCodeModel', AuthCodeSchema); 45 | -------------------------------------------------------------------------------- /generators/authentication/templates/src/config/oauth/clientModel.ts: -------------------------------------------------------------------------------- 1 | import { Document, Schema } from 'mongoose'; 2 | import * as OAuth2Server from 'oauth2-server'; 3 | import * as connections from '../connection/connection'; 4 | 5 | /** 6 | * @export 7 | * @interface IClientModel 8 | * @extends {Document} 9 | */ 10 | export interface IClientModel extends Document, OAuth2Server.Client { 11 | id: string; 12 | } 13 | 14 | const ClientSchema: Schema = new Schema({ 15 | id: { 16 | type: String, 17 | }, 18 | secret: { 19 | type: String, 20 | required: true, 21 | }, 22 | type: { 23 | type: String, 24 | enum: ['confidential', 'public'], 25 | default: 'confidential', 26 | }, 27 | redirectUris: { 28 | type: Array, 29 | required: true, 30 | }, 31 | grants: { 32 | type: Array, 33 | required: true, 34 | }, 35 | key: { 36 | type: String, 37 | }, 38 | }); 39 | 40 | export default connections.db.model< IClientModel >('ClientModel', ClientSchema); 41 | -------------------------------------------------------------------------------- /generators/authentication/templates/src/config/oauth/index.ts: -------------------------------------------------------------------------------- 1 | import * as OAuth2Server from 'oauth2-server'; 2 | import AuthCodeModel, { IAuthCodeModel } from './authCodeModel'; 3 | import ClientModel, { IClientModel } from './clientModel'; 4 | import TokenModel from './tokenModel'; 5 | 6 | interface IOAuth2ServerModel extends OAuth2Server.AuthorizationCodeModel, OAuth2Server.RefreshTokenModel { 7 | 8 | } 9 | interface IResultDeleteOne { 10 | deletedCount: Number; 11 | } 12 | // YOU CAN DELETE THIS BLOCK AFTER CLIENT CREATED 13 | const redirectUris: Array = '<%= redirectUris %>'.split(',').map((item) => { 14 | return item.trim(); 15 | }); 16 | 17 | const defaultClient: IClientModel = new ClientModel({ 18 | id: '<%= clientId %>', 19 | secret: '<%= clientSecret %>', 20 | type: 'confidential', 21 | redirectUris, 22 | grants: ['authorization_code', 'refresh_token'] 23 | }); 24 | 25 | defaultClient.save((err: Error) => { 26 | if (err) { 27 | console.log('error during creating defaultClient'); 28 | console.log(err); 29 | } else { 30 | console.log('created defaultClient'); 31 | } 32 | }); 33 | /** 34 | * OAuthServerModel 35 | * 36 | * @swagger 37 | * components: 38 | * securitySchemes: 39 | * oAuth2AuthCode: 40 | * type: oauth2 41 | * flows: 42 | * authorizationCode: 43 | * authorizationUrl: /auth/login 44 | * tokenUrl: /auth/token 45 | * scopes: {} 46 | */ 47 | const OAuth2ServerModel: IOAuth2ServerModel = { 48 | /** 49 | * @param {string} clientId 50 | * @param {clientSecret} clientSecret 51 | * @returns {Promise} 52 | */ 53 | getClient: async (clientId: string): Promise < OAuth2Server.Client | OAuth2Server.Falsey > => { 54 | try { 55 | return await ClientModel.findOne({ 56 | id: clientId, 57 | }); 58 | } catch (error) { 59 | throw new Error(error); 60 | } 61 | }, 62 | 63 | /** 64 | * @param {OAuth2Server.Token} token 65 | * @param {OAuth2Server.Client} client 66 | * @param {OAuth2Server.User} user 67 | * @returns {Promise} 68 | */ 69 | saveToken: async ( 70 | token: OAuth2Server.Token, 71 | client: OAuth2Server.Client, 72 | user: OAuth2Server.User, 73 | ): Promise < OAuth2Server.Token | OAuth2Server.Falsey > => { 74 | const oAuthtoken: OAuth2Server.Token = { 75 | user, 76 | accessToken: token.accessToken, 77 | accessTokenExpiresAt: token.accessTokenExpiresAt, 78 | refreshToken: token.refreshToken, 79 | refreshTokenExpiresAt: token.refreshTokenExpiresAt, 80 | scope: token.scope, 81 | client: { 82 | id: client.id, 83 | grants: client.grants, 84 | }, 85 | }; 86 | 87 | return TokenModel.create(oAuthtoken); 88 | }, 89 | 90 | /** 91 | * @param {string} accessToken 92 | * @returns {Promise} 93 | */ 94 | getAccessToken: async (accessToken: string): Promise < OAuth2Server.Token | OAuth2Server.Falsey > => { 95 | try { 96 | return await TokenModel.findOne({ 97 | accessToken, 98 | }); 99 | } catch (error) { 100 | throw new Error(error); 101 | } 102 | }, 103 | 104 | /** 105 | * @param {OAuth2Server.Token} token 106 | * @param {sring | string[]} scope 107 | * @returns {Promise} 108 | */ 109 | verifyScope: async (token: OAuth2Server.Token, scope: string | string[]): Promise < boolean > => { 110 | return token.scope === scope; 111 | }, 112 | 113 | /** 114 | * @param {string} authorizationCode 115 | * @returns {Promise} 116 | */ 117 | getAuthorizationCode: async (authorizationCode: string): Promise < OAuth2Server.AuthorizationCode | OAuth2Server.Falsey > => { 118 | try { 119 | const authCode: IAuthCodeModel = await AuthCodeModel.findOne({ 120 | authorizationCode, 121 | }); 122 | 123 | const code: OAuth2Server.AuthorizationCode = { 124 | authorizationCode: authCode.authorizationCode, 125 | expiresAt: authCode.expiresAt, 126 | redirectUri: authCode.redirectUri, 127 | scope: authCode.scope, 128 | client: { 129 | id: authCode.client.id, 130 | grants: authCode.client.grants, 131 | }, 132 | user: authCode.user, 133 | }; 134 | 135 | return code; 136 | } catch (error) { 137 | throw new Error(error); 138 | } 139 | }, 140 | 141 | /** 142 | * @param {Auth2Server.AuthorizationCode} code 143 | * @param {Auth2Server.Client} client 144 | * @param {Auth2Server.User} user 145 | * @returns {Promise} 146 | */ 147 | saveAuthorizationCode: async ( 148 | code: OAuth2Server.AuthorizationCode, 149 | client: OAuth2Server.Client, 150 | user: OAuth2Server.User, 151 | ): Promise < OAuth2Server.AuthorizationCode | OAuth2Server.Falsey > => { 152 | const authorizationCode: OAuth2Server.AuthorizationCode = { 153 | client: { 154 | id: client.id, 155 | grants: client.grants, 156 | }, 157 | user: user.id, 158 | scope: code.scope, 159 | authorizationCode: code.authorizationCode, 160 | expiresAt: code.expiresAt, 161 | redirectUri: code.redirectUri, 162 | }; 163 | 164 | return AuthCodeModel.create(authorizationCode); 165 | }, 166 | 167 | /** 168 | * @param {OAuth2Server.AuthorizationCode} authorizationCode 169 | * @returns {Promise} 170 | */ 171 | revokeAuthorizationCode: async (authorizationCode: OAuth2Server.AuthorizationCode): Promise < boolean > => { 172 | try { 173 | const result: IResultDeleteOne = await AuthCodeModel.deleteOne({ 174 | authorizationCode: authorizationCode.authorizationCode, 175 | }); 176 | 177 | return result.deletedCount > 0; 178 | } catch (error) { 179 | throw new Error(error); 180 | } 181 | }, 182 | 183 | /** 184 | * @param {string} refreshToken 185 | * @returns {Promise} 186 | */ 187 | getRefreshToken: async (refreshToken: string): Promise < OAuth2Server.RefreshToken | OAuth2Server.Falsey > => { 188 | try { 189 | return await TokenModel.findOne({ 190 | refreshToken, 191 | }); 192 | } catch (error) { 193 | throw new Error(error); 194 | } 195 | }, 196 | 197 | /** 198 | * @param {OAuth2Server.RefreshToken} token 199 | * @returns {Promise} 200 | */ 201 | revokeToken: async (token: OAuth2Server.RefreshToken): Promise < boolean > => { 202 | try { 203 | const result: IResultDeleteOne = await TokenModel.deleteOne({ 204 | refreshToken: token.refreshToken, 205 | }); 206 | 207 | return result.deletedCount > 0; 208 | } catch (error) { 209 | throw new Error(error); 210 | } 211 | }, 212 | }; 213 | 214 | /** 215 | * @exports 216 | */ 217 | export default new OAuth2Server({ 218 | accessTokenLifetime: 12 * 60 * 60, 219 | allowBearerTokensInQueryString: true, 220 | model: OAuth2ServerModel, 221 | }); 222 | -------------------------------------------------------------------------------- /generators/authentication/templates/src/config/oauth/tokenModel.ts: -------------------------------------------------------------------------------- 1 | import * as OAuth2Server from 'oauth2-server'; 2 | import { Document, Schema, Types } from 'mongoose'; 3 | import * as connections from '../connection/connection'; 4 | import { IUserModel } from '../../components/User/model'; 5 | 6 | /** 7 | * @exports 8 | * @interface ITokenModel 9 | * @extends {Document} 10 | */ 11 | export interface ITokenModel extends Document, OAuth2Server.Token { 12 | accessToken: string; 13 | accessTokenExpiresAt: Date; 14 | refreshToken: string; 15 | refreshTokenExpiresAt: Date; 16 | scope: string | string[]; 17 | user: IUserModel | Types.ObjectId | OAuth2Server.User; 18 | } 19 | 20 | const TokenSchema: Schema = new Schema({ 21 | accessToken: { 22 | type: String, 23 | required: true, 24 | }, 25 | accessTokenExpiresAt: { 26 | type: Date, 27 | required: true, 28 | }, 29 | refreshToken: { 30 | type: String, 31 | }, 32 | refreshTokenExpiresAt: { 33 | type: Date, 34 | }, 35 | scope: { 36 | type: String, 37 | }, 38 | client: { 39 | type: Object, 40 | required: true, 41 | }, 42 | user: { 43 | type: Object, 44 | required: true, 45 | }, 46 | }); 47 | 48 | export default connections.db.model< ITokenModel >('TokenModel', TokenSchema); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-node-express-typescript-api", 3 | "version": "2.6.3", 4 | "description": "This generator will help you to build your own Node.js Express Mongodb API using TypeScript.", 5 | "homepage": "https://github.com/ChechaValerii/node-typescript-mongodb#readme", 6 | "author": { 7 | "name": "Checha Valerii", 8 | "email": "chechavalera@gmail.com", 9 | "url": "https://github.com/ChechaValerii" 10 | }, 11 | "files": [ 12 | "generators" 13 | ], 14 | "main": "generators/index.js", 15 | "keywords": [ 16 | "yeoman-generator", 17 | "typescript", 18 | "node", 19 | "nodejs", 20 | "rest", 21 | "restfull", 22 | "api", 23 | "builder", 24 | "generator", 25 | "mongo", 26 | "ts", 27 | "starter", 28 | "mongodb", 29 | "auth", 30 | "js", 31 | "swagger" 32 | ], 33 | "devDependencies": { 34 | "eslint": "^4.19.1", 35 | "eslint-config-prettier": "^2.9.0", 36 | "eslint-config-xo": "^0.20.1", 37 | "eslint-plugin-prettier": "^2.6.0", 38 | "husky": "^0.14.3", 39 | "jest": "^22.0.6", 40 | "lint-staged": "^6.1.1", 41 | "nsp": "^3.2.1", 42 | "prettier": "^1.11.1", 43 | "yeoman-assert": "^3.1.0", 44 | "yeoman-test": "^1.7.0" 45 | }, 46 | "engines": { 47 | "npm": ">= 4.0.0" 48 | }, 49 | "dependencies": { 50 | "yeoman-generator": "^2.0.1", 51 | "chalk": "^2.1.0", 52 | "yosay": "^2.0.1" 53 | }, 54 | "jest": { 55 | "testEnvironment": "node" 56 | }, 57 | "scripts": { 58 | "pretest": "eslint .", 59 | "test": "jest" 60 | }, 61 | "lint-staged": { 62 | "*.js": [ 63 | "eslint --fix", 64 | "git add" 65 | ], 66 | "*.json": [ 67 | "prettier --write", 68 | "git add" 69 | ] 70 | }, 71 | "eslintConfig": { 72 | "extends": [ 73 | "xo", 74 | "prettier" 75 | ], 76 | "env": { 77 | "jest": true, 78 | "node": true 79 | }, 80 | "rules": { 81 | "prettier/prettier": [ 82 | "error", 83 | { 84 | "singleQuote": true, 85 | "printWidth": 90 86 | } 87 | ] 88 | }, 89 | "plugins": [ 90 | "prettier" 91 | ] 92 | }, 93 | "repository": "https://github.com/ChechaValerii/node-typescript-mongodb", 94 | "license": "MIT" 95 | } 96 | --------------------------------------------------------------------------------