├── .development.env.sample ├── .dockerignore ├── .editorconfig ├── .eslintrc.js.bk ├── .gitignore ├── .prettierrc.bk ├── .vscode └── settings.json ├── .vuepress └── config.js ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── docs ├── architecture.md └── development.md ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── ormconfig.ts ├── package.json ├── src ├── app.module.ts ├── boilerplate.polyfill.ts ├── common │ ├── abstract.entity.ts │ ├── constants │ │ ├── order.ts │ │ └── role-type.ts │ └── dto │ │ ├── AbstractDto.ts │ │ ├── AbstractSearchDto.ts │ │ ├── PageMetaDto.ts │ │ └── PageOptionsDto.ts ├── decorators │ ├── auth-user.decorator.ts │ ├── roles.decorator.ts │ ├── transforms.decorator.ts │ └── validators.decorator.ts ├── exceptions │ ├── file-not-image.exception.ts │ └── user-not-found.exception.ts ├── filters │ ├── bad-request.filter.ts │ ├── constraint-errors.ts │ └── query-failed.filter.ts ├── guards │ ├── auth.guard.ts │ └── roles.guard.ts ├── interceptors │ └── auth-user-interceptor.service.ts ├── interfaces │ ├── IAwsConfig.ts │ └── IFile.ts ├── main.hmr.ts ├── main.ts ├── middlewares │ ├── context.middelware.ts │ └── index.ts ├── modules │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── dto │ │ │ ├── LoginPayloadDto.ts │ │ │ ├── TokenPayloadDto.ts │ │ │ ├── UserLoginDto.ts │ │ │ └── UserRegisterDto.ts │ │ └── jwt.strategy.ts │ ├── chat │ │ ├── chat.controller.ts │ │ ├── chat.gateway.ts │ │ ├── chat.module.ts │ │ ├── chat.service.ts │ │ ├── dto │ │ │ ├── MessageDto.ts │ │ │ ├── RoomDto.ts │ │ │ ├── createMessageDto.ts │ │ │ ├── createPrivateMessageDto.ts │ │ │ ├── createPrivateRoomDto.ts │ │ │ ├── createRoomDto.ts │ │ │ └── joinRoomDto.ts │ │ ├── message.entity.ts │ │ ├── message.repository.ts │ │ ├── room.entity.ts │ │ └── room.repository.ts │ ├── math │ │ ├── math.controller.ts │ │ └── math.module.ts │ └── user │ │ ├── dto │ │ ├── UserDto.ts │ │ ├── UsersPageDto.ts │ │ └── UsersPageOptionsDto.ts │ │ ├── password.transformer.ts │ │ ├── user.controller.ts │ │ ├── user.entity.ts │ │ ├── user.module.ts │ │ ├── user.repository.ts │ │ └── user.service.ts ├── providers │ ├── context.service.ts │ └── utils.service.ts ├── shared │ ├── services │ │ ├── aws-s3.service.ts │ │ ├── config.service.ts │ │ ├── generator.service.ts │ │ └── validator.service.ts │ └── shared.module.ts ├── snake-naming.strategy.ts └── viveo-swagger.ts ├── static ├── index.html ├── main.js └── styles.css ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.development.env.sample: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | TRANSPORT_PORT=4000 3 | JWT_SECRET_KEY= 4 | JWT_EXPIRATION_TIME=3600 5 | 6 | # DATABASE envioroment variables 7 | DATABASE_TYPE=postgres 8 | DATABASE_HOST=localhost 9 | DATABASE_PORT=5432 10 | DATABASE_USERNAME= 11 | DATABASE_PASSWORD= 12 | DATABASE_DATABASE= 13 | 14 | # aws configurations 15 | ## AWS S3 16 | AWS_S3_ACCESS_KEY_ID= 17 | AWS_S3_SECRET_ACCESS_KEY= 18 | S3_BUCKET_NAME=bolerplate-bucket 19 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | dist 3 | node_modules 4 | db-data 5 | .vuepress 6 | .vscode 7 | docs 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.yml] 11 | indent_size = 2 12 | 13 | [*.md] 14 | max_line_length = off 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc.js.bk: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 'env': { 5 | 'browser': true, 6 | 'es6': true, 7 | 'node': true, 8 | }, 9 | 'parser': '@typescript-eslint/parser', 10 | 'parserOptions': { 11 | 'project': path.resolve(__dirname, "./tsconfig.json"), 12 | 'sourceType': 'module', 13 | }, 14 | extends: [ 15 | 'plugin:import/errors', 16 | 'plugin:import/warnings', 17 | 'plugin:import/typescript', 18 | "prettier/@typescript-eslint", 19 | "plugin:prettier/recommended" 20 | ], 21 | 'plugins': [ 22 | '@typescript-eslint', 23 | '@typescript-eslint/tslint', 24 | 'prettier', 25 | 'simple-import-sort', 26 | ], 27 | 'rules': { 28 | '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }], 29 | '@typescript-eslint/adjacent-overload-signatures': 'error', 30 | '@typescript-eslint/array-type': 'error', 31 | '@typescript-eslint/ban-types': 'error', 32 | '@typescript-eslint/class-name-casing': 'error', 33 | '@typescript-eslint/explicit-member-accessibility': [ 34 | 'off', 35 | { 36 | 'overrides': { 37 | 'constructors': 'off', 38 | }, 39 | }, 40 | ], 41 | '@typescript-eslint/indent': 'off', 42 | '@typescript-eslint/member-delimiter-style': [ 43 | 'error', 44 | { 45 | 'multiline': { 46 | 'delimiter': 'semi', 47 | 'requireLast': true, 48 | }, 49 | 'singleline': { 50 | 'delimiter': 'semi', 51 | 'requireLast': false, 52 | }, 53 | }, 54 | ], 55 | 'simple-import-sort/sort': 'error', 56 | '@typescript-eslint/member-ordering': 'off', 57 | '@typescript-eslint/no-angle-bracket-type-assertion': 'off', 58 | '@typescript-eslint/no-empty-function': 'error', 59 | '@typescript-eslint/no-empty-interface': 'error', 60 | '@typescript-eslint/no-explicit-any': 'off', 61 | '@typescript-eslint/no-inferrable-types': 'error', 62 | '@typescript-eslint/no-misused-new': 'error', 63 | '@typescript-eslint/no-namespace': 'error', 64 | '@typescript-eslint/no-require-imports': 'error', 65 | '@typescript-eslint/no-this-alias': 'error', 66 | '@typescript-eslint/no-use-before-declare': 'off', 67 | '@typescript-eslint/no-var-requires': 'error', 68 | '@typescript-eslint/prefer-for-of': 'error', 69 | '@typescript-eslint/prefer-function-type': 'error', 70 | '@typescript-eslint/prefer-namespace-keyword': 'error', 71 | '@typescript-eslint/quotes': [ 72 | 'error', 73 | 'single', 74 | { 75 | 'avoidEscape': true, 76 | }, 77 | ], 78 | '@typescript-eslint/semi': [ 79 | 'error', 80 | 'always', 81 | ], 82 | '@typescript-eslint/type-annotation-spacing': 'error', 83 | '@typescript-eslint/unified-signatures': 'error', 84 | 'arrow-body-style': 'error', 85 | 'arrow-parens': [ 86 | 'error', 87 | 'as-needed', 88 | ], 89 | 'camelcase': 'error', 90 | 'complexity': 'off', 91 | 'constructor-super': 'error', 92 | 'curly': 'error', 93 | 'dot-notation': 'error', 94 | 'eol-last': 'error', 95 | 'eqeqeq': [ 96 | 'error', 97 | 'smart', 98 | ], 99 | 'guard-for-in': 'error', 100 | 'id-match': 'error', 101 | 'import/no-default-export': 'error', 102 | 'import/no-internal-modules': 'off', 103 | 'import/order': 'off', 104 | 'max-classes-per-file': [ 105 | 'error', 106 | 1, 107 | ], 108 | 'max-len': [ 109 | 'error', 110 | { 111 | 'code': 150, 112 | }, 113 | ], 114 | 'new-parens': 'error', 115 | 'no-bitwise': 'error', 116 | 'no-caller': 'error', 117 | 'no-cond-assign': 'error', 118 | 'no-console': [ 119 | 'error', 120 | { 121 | 'allow': [ 122 | 'info', 123 | 'dirxml', 124 | 'warn', 125 | 'error', 126 | 'dir', 127 | 'timeLog', 128 | 'assert', 129 | 'clear', 130 | 'count', 131 | 'countReset', 132 | 'group', 133 | 'groupCollapsed', 134 | 'groupEnd', 135 | 'table', 136 | 'Console', 137 | 'markTimeline', 138 | 'profile', 139 | 'profileEnd', 140 | 'timeline', 141 | 'timelineEnd', 142 | 'timeStamp', 143 | 'context', 144 | ], 145 | }, 146 | ], 147 | 'no-debugger': 'error', 148 | 'no-duplicate-case': 'error', 149 | 'no-duplicate-imports': 'error', 150 | 'no-empty': 'error', 151 | 'no-eval': 'error', 152 | 'no-extra-bind': 'error', 153 | 'no-fallthrough': 'error', 154 | 'no-invalid-this': 'error', 155 | 'no-irregular-whitespace': 'error', 156 | 'no-multiple-empty-lines': [ 157 | 'error', 158 | { 159 | 'max': 1, 160 | }, 161 | ], 162 | 'no-new-func': 'error', 163 | 'no-new-wrappers': 'error', 164 | 'no-redeclare': 'error', 165 | 'no-return-await': 'error', 166 | 'no-sequences': 'error', 167 | 'no-shadow': [ 168 | 'error', 169 | { 170 | 'hoist': 'all', 171 | }, 172 | ], 173 | 'no-sparse-arrays': 'error', 174 | 'no-template-curly-in-string': 'error', 175 | 'no-throw-literal': 'error', 176 | 'no-trailing-spaces': 'error', 177 | 'no-undef-init': 'error', 178 | 'no-unsafe-finally': 'error', 179 | 'no-unused-expressions': 'error', 180 | 'no-unused-labels': 'error', 181 | 'no-var': 'error', 182 | 'object-shorthand': 'error', 183 | 'prefer-const': 'error', 184 | 'prefer-object-spread': 'error', 185 | 'quote-props': [ 186 | 'error', 187 | 'consistent-as-needed', 188 | ], 189 | 'radix': 'error', 190 | 'space-before-function-paren': [ 191 | 'error', 192 | { 193 | 'anonymous': 'never', 194 | 'named': 'never', 195 | 'asyncArrow': 'always', 196 | }, 197 | ], 198 | 'use-isnan': 'error', 199 | 'valid-typeof': 'off', 200 | '@typescript-eslint/tslint/config': [ 201 | 'error', 202 | { 203 | 'rulesDirectory': [ 204 | './node_modules/tslint-eslint-rules/dist/rules', 205 | './node_modules/tslint-config-prettier/lib', 206 | './node_modules/tslint-consistent-codestyle/rules', 207 | ], 208 | 'rules': { 209 | 'align': [ 210 | true, 211 | 'parameters', 212 | 'statements', 213 | 'members', 214 | ], 215 | 'comment-format': [ 216 | true, 217 | 'check-space', 218 | ], 219 | 'import-spacing': true, 220 | 'jsdoc-format': [ 221 | true, 222 | 'check-multiline-start', 223 | ], 224 | 'naming-convention': [ 225 | true, 226 | { 227 | 'type': 'default', 228 | 'format': 'camelCase', 229 | 'leadingUnderscore': 'forbid', 230 | 'trailingUnderscore': 'forbid', 231 | }, 232 | { 233 | 'type': 'variable', 234 | 'modifiers': [ 235 | 'global', 236 | 'const', 237 | ], 238 | 'format': [ 239 | 'camelCase', 240 | 'PascalCase', 241 | 'UPPER_CASE', 242 | ], 243 | }, 244 | { 245 | 'type': 'parameter', 246 | 'modifiers': 'unused', 247 | 'leadingUnderscore': 'allow', 248 | }, 249 | { 250 | 'type': 'member', 251 | 'modifiers': 'private', 252 | 'leadingUnderscore': 'require', 253 | }, 254 | { 255 | 'type': 'member', 256 | 'modifiers': 'protected', 257 | 'leadingUnderscore': 'require', 258 | }, 259 | { 260 | 'type': 'property', 261 | 'modifiers': [ 262 | 'public', 263 | 'static', 264 | 'const', 265 | ], 266 | 'format': 'UPPER_CASE', 267 | }, 268 | { 269 | 'type': 'type', 270 | 'format': 'PascalCase', 271 | }, 272 | { 273 | 'type': 'interface', 274 | 'prefix': 'I', 275 | }, 276 | { 277 | 'type': 'genericTypeParameter', 278 | 'regex': '^[A-Z]$', 279 | }, 280 | { 281 | 'type': 'enumMember', 282 | 'format': 'UPPER_CASE', 283 | }, 284 | ], 285 | 'no-accessor-recursion': true, 286 | 'no-as-type-assertion': true, 287 | 'no-collapsible-if': true, 288 | 'no-implicit-dependencies': true, 289 | 'no-multi-spaces': true, 290 | 'no-reference-import': true, 291 | 'no-return-undefined': [ 292 | true, 293 | 'allow-void-expression', 294 | ], 295 | 'no-unnecessary-callback-wrapper': true, 296 | 'no-unnecessary-else': true, 297 | 'no-unnecessary-type-annotation': true, 298 | 'no-var-before-return': true, 299 | 'number-literal-format': true, 300 | 'object-shorthand-properties-first': true, 301 | 'one-line': [ 302 | true, 303 | 'check-open-brace', 304 | 'check-catch', 305 | 'check-else', 306 | 'check-finally', 307 | 'check-whitespace', 308 | ], 309 | 'parameter-properties': [ 310 | true, 311 | 'leading', 312 | 'member-access', 313 | ], 314 | 'prefer-conditional-expression': true, 315 | 'prefer-const-enum': true, 316 | 'prefer-switch': [ 317 | true, 318 | { 319 | 'min-cases': 3, 320 | }, 321 | ], 322 | 'prefer-while': true, 323 | 'switch-final-break': true, 324 | 'trailing-comma': [ 325 | true, 326 | { 327 | 'singleline': 'never', 328 | 'multiline': 'always', 329 | }, 330 | ], 331 | 'whitespace': [ 332 | true, 333 | 'check-branch', 334 | 'check-decl', 335 | 'check-operator', 336 | 'check-separator', 337 | 'check-type', 338 | 'check-type-operator', 339 | 'check-rest-spread', 340 | ], 341 | }, 342 | }, 343 | ], 344 | }, 345 | }; 346 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | ### Example user template template 65 | ### Example user template 66 | 67 | # IntelliJ project files 68 | .idea 69 | *.iml 70 | out 71 | gen 72 | dist 73 | mongo 74 | 75 | 76 | # Docker compose ganarated files 77 | db-data 78 | /src/migrations/ 79 | .development.env 80 | -------------------------------------------------------------------------------- /.prettierrc.bk: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Injectable", 4 | "microservices", 5 | "nestjs", 6 | "postgres" 7 | ], 8 | "editor.detectIndentation": false 9 | } -------------------------------------------------------------------------------- /.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Awesome nest boilerplate 🎉', 3 | description: `An ultimate and awesome nodejs boilerplate wrote in typescript`, 4 | base: process.env.DEPLOY_ENV === 'gh-pages' ? '/awesome-nest-boilerplate/' : '/', 5 | themeConfig: { 6 | sidebar: [ 7 | ['/', 'Introduction'], 8 | '/docs/development', 9 | '/docs/architecture', 10 | // '/docs/tech', 11 | // '/docs/routing', 12 | // '/docs/state', 13 | // '/docs/linting', 14 | // '/docs/editors', 15 | // '/docs/production', 16 | // '/docs/troubleshooting', 17 | ], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:dubnium AS dist 2 | COPY package.json yarn.lock ./ 3 | 4 | RUN yarn install 5 | 6 | COPY . ./ 7 | 8 | RUN yarn build 9 | 10 | FROM node:dubnium AS node_modules 11 | COPY package.json yarn.lock ./ 12 | 13 | RUN yarn install --prod 14 | 15 | FROM node:dubnium 16 | 17 | ARG PORT=3000 18 | 19 | RUN mkdir -p /usr/src/app 20 | 21 | WORKDIR /usr/src/app 22 | 23 | COPY --from=dist dist /usr/src/app/dist 24 | COPY --from=node_modules node_modules /usr/src/app/node_modules 25 | 26 | COPY . /usr/src/app 27 | 28 | EXPOSE $PORT 29 | 30 | CMD [ "yarn", "start:prod" ] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Narek Hakobyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nest.js Chat Application 2 | 3 | ## features: 4 | 1. private chat ( one to one ) 5 | 2. public chat 6 | 3. create public chat groups that others can join and message 7 | 4. store in database (postgresql) 8 | 5. jwt authentication 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | app: 4 | env_file: 5 | - .development.env 6 | container_name: awesome_nest_boilerplate 7 | restart: always 8 | build: . 9 | ports: 10 | - "$PORT:$PORT" 11 | links: 12 | - postgres 13 | postgres: 14 | image: postgres 15 | restart: always 16 | environment: 17 | POSTGRES_PASSWORD: postgres 18 | ports: 19 | - "5433:5432" 20 | volumes: 21 | - ./db-data:/var/lib/postgresql/data 22 | adminer: 23 | image: adminer 24 | restart: always 25 | ports: 26 | - 8080:8080 27 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | - [Architecture](#architecture) 4 | - [`.vscode`](#vscode) 5 | - [`docs`](#docs) 6 | - [`.vuepress`](#vuepress) 7 | - [`src`](#src) 8 | - [`common`](#common) 9 | - [`decorators`](#decorators) 10 | - [`interceptors`](#interceptors) 11 | - [`exception-filters`](#exception-filters) 12 | - [`guards`](#guards) 13 | - [`interfaces`](#interfaces) 14 | - [`migrations`](#migrations) 15 | - [`providers`](#providers) 16 | - [`shared`](#shared) 17 | - [`modules`](#modules) 18 | - [`app.module.ts`](#appmodulets) 19 | - [`boilerplate.polyfill.ts`](#boilerplatepolyfillts) 20 | - [`snake-naming.strategy.ts`](#snake-namingstrategyts) 21 | - [`.*.env`](#env) 22 | - [`.eslintrc.json`](#eslintrcjson) 23 | - [`tslint.json`](#tslintjson) 24 | 25 | ## `.vscode` 26 | 27 | Settings and extensions specific to this project, for Visual Studio Code. See [the editors doc](editors.md#visual-studio-code) for more. 28 | 29 | ## `docs` 30 | 31 | You found me! :wink: 32 | 33 | ## `.vuepress` 34 | 35 | Documentation config and destination folder See [VuePress doc](https://vuepress.vuejs.org) for more 36 | 37 | ## `src` 38 | 39 | Where we keep all our source files. 40 | 41 | ### `common` 42 | 43 | Where we keep common typescript files, e.g. constants and DTOs. 44 | 45 | ### `decorators` 46 | 47 | This folder contains all global [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html). 48 | 49 | ### `interceptors` 50 | 51 | Where we are keep [interceptors](https://docs.nestjs.com/interceptors) 52 | 53 | ### `exception-filters` 54 | 55 | In this folder you can find app level [exception-filters](https://docs.nestjs.com/exception-filters). 56 | 57 | ### `guards` 58 | 59 | You can store all guards here 60 | 61 | ### `interfaces` 62 | 63 | This folder contains typescript [interfaces](https://www.typescriptlang.org/docs/handbook/interfaces.html) 64 | 65 | ### `migrations` 66 | 67 | Folder to store application migrations which will be generated by typeorm. 68 | 69 | ### `providers` 70 | 71 | These are utility functions you may want to share between many files in your application. They will always be pure and never have side effects, meaning if you provide a function the same arguments, it will always return the same result. 72 | 73 | ### `shared` 74 | 75 | Shared module with global singleton services. 76 | 77 | ### `modules` 78 | 79 | Where all our NestJS modules lives. See [NestJS modules documentation](https://docs.nestjs.com/modules) for more. 80 | 81 | ### `app.module.ts` 82 | 83 | The root application module. 84 | 85 | ### `boilerplate.polyfill.ts` 86 | 87 | We extend built in classes so you can use helper function anywhere. 88 | 89 | ```typescript 90 | const users: UserEntity[] = ...; 91 | 92 | const userDtos = users.toDtos(); 93 | ``` 94 | 95 | ### `snake-naming.strategy.ts` 96 | 97 | We are using snake naming strategy for typeorm, so when you will generate migration it automatically will set snake_case column name from entity fields. 98 | 99 | ## `.*.env` 100 | 101 | Environment variables which will load before app start and will be stored in `process.env`, (*) is a env name (development, staging, production, ...) 102 | 103 | ## `.eslintrc.json` 104 | 105 | Eslint configuration file, See [the eslint doc](https://eslint.org/) for more. 106 | 107 | ## `tslint.json` 108 | 109 | Tslint configuration file, See [the tslint doc](https://palantir.github.io/tslint/) for more. 110 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Setup and development 2 | 3 | - [Setup and development](#setup-and-development) 4 | - [First-time setup](#first-time-setup) 5 | - [Installation](#installation) 6 | - [Database](#database) 7 | - [Configuration](#configuration) 8 | - [Dev server](#dev-server) 9 | - [Generators](#generators) 10 | - [Docker](#docker) 11 | - [Docker installation](#docker-installation) 12 | - [Docker-compose installation](#docker-compose-installation) 13 | - [Run](#run) 14 | 15 | ## First-time setup 16 | 17 | Make sure you have the following installed: 18 | 19 | - [Node](https://nodejs.org/en/) (at least the latest LTS) 20 | - [Yarn](https://yarnpkg.com/lang/en/docs/install/) (at least 1.0) 21 | 22 | ## Installation 23 | 24 | ```bash 25 | # Install dependencies from package.json 26 | yarn install 27 | ``` 28 | 29 | > Note: don't delete yarn.lock before installation 30 | 31 | ### Database 32 | 33 | > Note: Awesome nest boilerplate uses [TypeORM](https://github.com/typeorm/typeorm) with Data Mapper pattern. 34 | 35 | ### Configuration 36 | 37 | Before start install PostgreSQL and fill correct configurations in `.development.env` file 38 | 39 | ```env 40 | POSTGRES_HOST=localhost 41 | POSTGRES_PORT=5432 42 | POSTGRES_USERNAME=postgres 43 | POSTGRES_PASSWORD=postgres 44 | POSTGRES_DATABASE=nest_boilerplate 45 | ``` 46 | 47 | Some helper script to work with database 48 | 49 | ```bash 50 | # To create new migration file 51 | yarn typeorm:migration:create migration_name 52 | 53 | # Truncate full database (note: it isn't deleting the database) 54 | yarn typeorm:schema:drop 55 | 56 | # Generate migration from update of entities 57 | yarn migration:generate migration_name 58 | ``` 59 | 60 | ### Dev server 61 | 62 | > Note: If you're on Linux and see an `ENOSPC` error when running the commands below, you must [increase the number of available file watchers](https://stackoverflow.com/questions/22475849/node-js-error-enospc#answer-32600959). 63 | 64 | ```bash 65 | # Launch the dev server 66 | yarn start:dev 67 | 68 | # Launch the dev server with file watcher 69 | yarn watch:dev 70 | 71 | # Launch the dev server and enable remote debugger with file watcher 72 | yarn debug:dev 73 | ``` 74 | 75 | ## Generators 76 | 77 | This project includes generators to speed up common development tasks. Commands include: 78 | 79 | > Note: Make sure you already have the nest-cli globally installed 80 | 81 | ```bash 82 | # Install nest-cli globally 83 | yarn global add @nestjs/cli 84 | 85 | # Generate a new service 86 | nest generate service users 87 | 88 | # Generate a new class 89 | nest g class users 90 | 91 | ``` 92 | > Note: if you love generators then you can find full list of command in official [Nest-cli Docs](https://docs.nestjs.com/cli/usages#generate-alias-g). 93 | 94 | ## Docker 95 | 96 | if you are familiar with [docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose) then you can run built in docker-compose file, which will install and configure application and database for you. 97 | 98 | ### Docker installation 99 | 100 | Download docker from Official website 101 | 102 | - Mac 103 | - Windows 104 | - Ubuntu 105 | 106 | ### Docker-compose installation 107 | 108 | Download docker from [Official website](https://docs.docker.com/compose/install) 109 | 110 | ### Run 111 | 112 | Open terminal and navigate to project directory and run the following command. 113 | 114 | ```bash 115 | PORT=3000 docker-compose up 116 | ``` 117 | 118 | > Note: application will run on port 3000 () 119 | 120 | Navigate to and connect to you database with the following configurations 121 | 122 | ```text 123 | host: postgres 124 | user: postgres 125 | pass: postgres 126 | ``` 127 | 128 | create database `nest_boilerplate` and your application fully is ready to use. 129 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "TS_NODE_CACHE=false node --inspect -r ts-node/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /ormconfig.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import { SnakeNamingStrategy } from './src/snake-naming.strategy'; 3 | import './src/boilerplate.polyfill'; 4 | 5 | if (!(module).hot /* for webpack HMR */) { 6 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 7 | } 8 | 9 | dotenv.config({ 10 | path: `.${process.env.NODE_ENV}.env`, 11 | }); 12 | 13 | // Replace \\n with \n to support multiline strings in AWS 14 | for (const envName of Object.keys(process.env)) { 15 | process.env[envName] = process.env[envName].replace(/\\n/g, '\n'); 16 | } 17 | 18 | module.exports = { 19 | type: 'postgres', 20 | host: process.env.DATABASE_HOST, 21 | port: +process.env.DATABASE_PORT, 22 | username: process.env.DATABASE_USERNAME, 23 | password: process.env.DATABASE_PASSWORD, 24 | database: process.env.DATABASE_DATABASE, 25 | namingStrategy: new SnakeNamingStrategy(), 26 | entities: [ 27 | 'src/modules/**/*.entity{.ts,.js}', 28 | ], 29 | migrations: [ 30 | 'src/migrations/*{.ts,.js}', 31 | ], 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awesome-nest-boilerplate", 3 | "version": "0.0.1", 4 | "description": "Awesome NestJS Boilerplate, Typescript, Postgres, TypeORM", 5 | "author": "Narek Hakobyan ", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.build.json", 9 | "format": "prettier --write \"src/**/*.ts\"", 10 | "start:hmr": "node dist/main", 11 | "start:dev": "ts-node -r tsconfig-paths/register src/main.ts", 12 | "migration:generate": "ts-node node_modules/typeorm/cli.js migration:generate -f ormconfig -d src/migrations -n", 13 | "migration:revert": "ts-node node_modules/typeorm/cli.js migration:revert -f ormconfig", 14 | "watch:dev": "nodemon --config nodemon.json", 15 | "debug:dev": "nodemon --config nodemon-debug.json", 16 | "webpack": "webpack --config webpack.config.js --progress", 17 | "migration:create": "ts-node node_modules/typeorm/cli.js migration:create -f ormconfig -d src/migrations -n", 18 | "schema:drop": "ts-node node_modules/typeorm/cli.js schema:drop -f ormconfig", 19 | "start:prod": "node dist/main.js", 20 | "lint": "eslint . --ext .ts", 21 | "lint:fix": "eslint --fix . --ext .ts", 22 | "test": "jest", 23 | "test:watch": "jest --watch", 24 | "test:cov": "jest --coverage", 25 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 26 | "test:e2e": "jest --config ./test/jest-e2e.json", 27 | "docs:dev": "vuepress dev -p 7070", 28 | "docs:build": "DEPLOY_ENV=gh-pages vuepress build", 29 | "docs:deploy": "yarn docs:build && gh-pages -d .vuepress/dist" 30 | }, 31 | "dependencies": { 32 | "@nestjs/common": "^6.8.3", 33 | "@nestjs/core": "^6.8.3", 34 | "@nestjs/jwt": "^6.1.1", 35 | "@nestjs/microservices": "^6.8.3", 36 | "@nestjs/passport": "^6.1.0", 37 | "@nestjs/platform-express": "^6.8.3", 38 | "@nestjs/platform-socket.io": "^6.11.6", 39 | "@nestjs/serve-static": "^1.0.2", 40 | "@nestjs/swagger": "^3.1.0", 41 | "@nestjs/typeorm": "^6.2.0", 42 | "@nestjs/websockets": "^6.11.6", 43 | "aws-sdk": "~2.549.0", 44 | "bcrypt": "~3.0.6", 45 | "class-transformer": "^0.2.3", 46 | "class-validator": "^0.10.2", 47 | "compression": "~1.7.4", 48 | "dotenv": "~8.1.0", 49 | "express": "~4.17.1", 50 | "express-rate-limit": "~5.0.0", 51 | "file-type": "~12.3.0", 52 | "helmet": "~3.21.1", 53 | "jsonwebtoken": "~8.5.1", 54 | "lodash": "^4.17.15", 55 | "mime-types": "~2.1.24", 56 | "morgan": "~1.9.1", 57 | "mysql": "^2.18.1", 58 | "nestjs-redis": "^1.2.5", 59 | "passport": "~0.4.0", 60 | "passport-jwt": "^4.0.0", 61 | "pg": "^7.12.1", 62 | "redis": "^3.0.2", 63 | "reflect-metadata": "~0.1.13", 64 | "request-context": "~2.0.0", 65 | "rxjs": "^6.5.3", 66 | "socket.io-redis": "^5.2.0", 67 | "socketio-jwt": "^4.5.0", 68 | "source-map-support": "^0.5.13", 69 | "swagger-ui-express": "^4.1.2", 70 | "typeorm": "^0.2.19", 71 | "typeorm-transactional-cls-hooked": "^0.1.8", 72 | "typescript": "^3.6.4", 73 | "uuid": "^3.3.3" 74 | }, 75 | "devDependencies": { 76 | "@nestjs/testing": "^6.8.3", 77 | "@types/bcrypt": "^3.0.0", 78 | "@types/compression": "^1.0.1", 79 | "@types/dotenv": "^6.1.1", 80 | "@types/express": "^4.17.1", 81 | "@types/express-rate-limit": "^3.3.3", 82 | "@types/file-type": "^10.9.1", 83 | "@types/helmet": "^0.0.44", 84 | "@types/jest": "^24.0.19", 85 | "@types/jsonwebtoken": "^8.3.4", 86 | "@types/lodash": "^4.14.144", 87 | "@types/mime-types": "^2.1.0", 88 | "@types/morgan": "^1.7.37", 89 | "@types/node": "^12.7.12", 90 | "@types/passport-jwt": "^3.0.2", 91 | "@types/socket.io": "^2.1.4", 92 | "@types/supertest": "^2.0.8", 93 | "@types/uuid": "^3.4.5", 94 | "@typescript-eslint/eslint-plugin": "^2.5.0", 95 | "@typescript-eslint/eslint-plugin-tslint": "^2.5.0", 96 | "@typescript-eslint/parser": "^2.5.0", 97 | "clean-webpack-plugin": "^3.0.0", 98 | "eslint": "^6.5.1", 99 | "eslint-config-prettier": "^6.4.0", 100 | "eslint-plugin-import": "^2.18.2", 101 | "eslint-plugin-import-helpers": "^1.0.2", 102 | "eslint-plugin-prettier": "^3.1.1", 103 | "eslint-plugin-simple-import-sort": "^5.0.0", 104 | "gh-pages": "^2.1.1", 105 | "husky": "^3.0.9", 106 | "jest": "^24.9.0", 107 | "lint-staged": "~9.4.2", 108 | "nodemon": "^1.19.3", 109 | "prettier": "^1.18.2", 110 | "supertest": "^4.0.2", 111 | "ts-jest": "^24.1.0", 112 | "ts-loader": "^6.2.0", 113 | "ts-node": "^8.4.1", 114 | "tsconfig-paths": "^3.9.0", 115 | "tslint": "^5.20.0", 116 | "tslint-config-prettier": "~1.18.0", 117 | "tslint-consistent-codestyle": "^1.16.0", 118 | "tslint-eslint-rules": "^5.4.0", 119 | "tslint-plugin-prettier": "^2.0.1", 120 | "vuepress": "^1.2.0", 121 | "webpack": "^4.41.1", 122 | "webpack-cli": "^3.3.9", 123 | "webpack-node-externals": "^1.7.2" 124 | }, 125 | "jest": { 126 | "moduleFileExtensions": [ 127 | "js", 128 | "json", 129 | "ts" 130 | ], 131 | "rootDir": "src", 132 | "testRegex": ".spec.ts$", 133 | "transform": { 134 | "^.+\\.(t|j)s$": "ts-jest" 135 | }, 136 | "coverageDirectory": "../coverage", 137 | "testEnvironment": "node" 138 | }, 139 | "husky": { 140 | "hooks": { 141 | "pre-commit": "lint-staged" 142 | } 143 | }, 144 | "lint-staged": { 145 | "*.ts": [ 146 | "eslint --fix", 147 | "git add" 148 | ] 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import './boilerplate.polyfill'; 2 | 3 | import {MiddlewareConsumer, Module, NestModule} from '@nestjs/common'; 4 | import {ServeStaticModule} from '@nestjs/serve-static'; 5 | import {TypeOrmModule} from '@nestjs/typeorm'; 6 | import {join} from 'path'; 7 | 8 | import {ChatModule} from './modules/chat/chat.module'; 9 | import {contextMiddleware} from './middlewares'; 10 | import {AuthModule} from './modules/auth/auth.module'; 11 | import {MathModule} from './modules/math/math.module'; 12 | import {UserModule} from './modules/user/user.module'; 13 | import {ConfigService} from './shared/services/config.service'; 14 | import {SharedModule} from './shared/shared.module'; 15 | import {RedisModule} from "nestjs-redis"; 16 | 17 | @Module({ 18 | imports: [ 19 | AuthModule, 20 | UserModule, 21 | MathModule, 22 | ChatModule, 23 | TypeOrmModule.forRootAsync({ 24 | imports: [SharedModule], 25 | useFactory: (configService: ConfigService) => 26 | configService.typeOrmConfig, 27 | inject: [ConfigService], 28 | }), 29 | ServeStaticModule.forRoot({ 30 | rootPath: join(__dirname, '..', 'static'), 31 | }), 32 | RedisModule.register({url: 'redis://127.0.0.1:6379/0'}), 33 | ], 34 | providers: [], 35 | }) 36 | export class AppModule implements NestModule { 37 | configure(consumer: MiddlewareConsumer): MiddlewareConsumer | void { 38 | consumer.apply(contextMiddleware).forRoutes('*'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/boilerplate.polyfill.ts: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | import * as _ from 'lodash'; 3 | 4 | import { AbstractEntity } from './common/abstract.entity'; 5 | import { AbstractDto } from './common/dto/AbstractDto'; 6 | 7 | declare global { 8 | // tslint:disable-next-line:naming-convention no-unused 9 | interface Array { 10 | toDtos(this: AbstractEntity[]): B[]; 11 | } 12 | } 13 | 14 | Array.prototype.toDtos = function(options?: any): B[] { 15 | // tslint:disable-next-line:no-invalid-this 16 | return _(this) 17 | .map(item => item.toDto(options)) 18 | .compact() 19 | .value(); 20 | }; 21 | -------------------------------------------------------------------------------- /src/common/abstract.entity.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | } from 'typeorm'; 8 | 9 | import { UtilsService } from '../providers/utils.service'; 10 | import { AbstractDto } from './dto/AbstractDto'; 11 | 12 | export abstract class AbstractEntity { 13 | @PrimaryGeneratedColumn('uuid') 14 | id: string; 15 | 16 | @CreateDateColumn({ 17 | type: 'timestamp without time zone', 18 | name: 'created_at', 19 | }) 20 | createdAt: Date; 21 | 22 | @UpdateDateColumn({ 23 | type: 'timestamp without time zone', 24 | name: 'updated_at', 25 | }) 26 | updatedAt: Date; 27 | 28 | abstract dtoClass: new (entity: AbstractEntity, options?: any) => T; 29 | 30 | toDto(options?: any) { 31 | return UtilsService.toDto(this.dtoClass, this, options); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/common/constants/order.ts: -------------------------------------------------------------------------------- 1 | export enum Order { 2 | ASC = 'ASC', 3 | DESC = 'DESC', 4 | } 5 | -------------------------------------------------------------------------------- /src/common/constants/role-type.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export enum RoleType { 4 | USER = 'USER', 5 | ADMIN = 'ADMIN', 6 | } 7 | -------------------------------------------------------------------------------- /src/common/dto/AbstractDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { AbstractEntity } from '../abstract.entity'; 4 | 5 | export class AbstractDto { 6 | id: string; 7 | createdAt: Date; 8 | updatedAt: Date; 9 | 10 | constructor(entity: AbstractEntity) { 11 | this.id = entity.id; 12 | this.createdAt = entity.createdAt; 13 | this.updatedAt = entity.updatedAt; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/common/dto/AbstractSearchDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { ApiModelProperty, ApiModelPropertyOptional } from '@nestjs/swagger'; 4 | import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; 5 | import { ToInt } from '../../decorators/transforms.decorator'; 6 | 7 | export class AbstractSearchDto { 8 | @ApiModelProperty() 9 | @IsString() 10 | @IsNotEmpty() 11 | q: string; 12 | 13 | @ApiModelProperty() 14 | @IsNumber() 15 | @IsNotEmpty() 16 | @ToInt() 17 | page: number; 18 | 19 | @ApiModelPropertyOptional() 20 | @IsNumber() 21 | @IsOptional() 22 | @ToInt() 23 | take = 10; 24 | 25 | get skip() { 26 | return (this.page - 1) * this.take; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/common/dto/PageMetaDto.ts: -------------------------------------------------------------------------------- 1 | import { ApiModelProperty } from '@nestjs/swagger'; 2 | import { PageOptionsDto } from './PageOptionsDto'; 3 | 4 | interface IPageMetaDtoParameters { 5 | pageOptionsDto: PageOptionsDto; 6 | itemCount: number; 7 | } 8 | 9 | export class PageMetaDto { 10 | @ApiModelProperty() 11 | readonly page: number; 12 | 13 | @ApiModelProperty() 14 | readonly take: number; 15 | 16 | @ApiModelProperty() 17 | readonly itemCount: number; 18 | 19 | @ApiModelProperty() 20 | readonly pageCount: number; 21 | 22 | constructor({ pageOptionsDto, itemCount }: IPageMetaDtoParameters) { 23 | this.page = pageOptionsDto.page; 24 | this.take = pageOptionsDto.take; 25 | this.itemCount = itemCount; 26 | this.pageCount = Math.ceil(itemCount / this.take); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/common/dto/PageOptionsDto.ts: -------------------------------------------------------------------------------- 1 | import { ApiModelPropertyOptional } from '@nestjs/swagger'; 2 | import { Type } from 'class-transformer'; 3 | import { 4 | IsEnum, 5 | IsInt, 6 | Min, 7 | IsOptional, 8 | Max, 9 | IsString, 10 | IsNotEmpty, 11 | } from 'class-validator'; 12 | 13 | import { Order } from '../constants/order'; 14 | 15 | export class PageOptionsDto { 16 | @ApiModelPropertyOptional({ 17 | enum: Order, 18 | default: Order.ASC, 19 | }) 20 | @IsEnum(Order) 21 | @IsOptional() 22 | readonly order: Order = Order.ASC; 23 | 24 | @ApiModelPropertyOptional({ 25 | minimum: 1, 26 | default: 1, 27 | }) 28 | @Type(() => Number) 29 | @IsInt() 30 | @Min(1) 31 | @IsOptional() 32 | readonly page: number = 1; 33 | 34 | @ApiModelPropertyOptional({ 35 | minimum: 1, 36 | maximum: 50, 37 | default: 10, 38 | }) 39 | @Type(() => Number) 40 | @IsInt() 41 | @Min(10) 42 | @Max(50) 43 | @IsOptional() 44 | readonly take: number = 10; 45 | 46 | get skip(): number { 47 | return (this.page - 1) * this.take; 48 | } 49 | 50 | @ApiModelPropertyOptional() 51 | @IsString() 52 | @IsNotEmpty() 53 | @IsOptional() 54 | readonly q?: string; 55 | } 56 | -------------------------------------------------------------------------------- /src/decorators/auth-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | 3 | export const AuthUser = createParamDecorator((_data, request) => request.user); 4 | -------------------------------------------------------------------------------- /src/decorators/roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | import { RoleType } from '../common/constants/role-type'; 4 | 5 | export const Roles = (...roles: RoleType[]) => SetMetadata('roles', roles); 6 | -------------------------------------------------------------------------------- /src/decorators/transforms.decorator.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:naming-convention */ 2 | 3 | import { Transform } from 'class-transformer'; 4 | import * as _ from 'lodash'; 5 | 6 | /** 7 | * @description trim spaces from start and end, replace multiple spaces with one. 8 | * @example 9 | * @ApiModelProperty() 10 | * @IsString() 11 | * @Trim() 12 | * name: string; 13 | * @returns {(target: any, key: string) => void} 14 | * @constructor 15 | */ 16 | export function Trim() { 17 | return Transform((value: string | string[]) => { 18 | if (_.isArray(value)) { 19 | return value.map(v => _.trim(v).replace(/\s\s+/g, ' ')); 20 | } 21 | return _.trim(value).replace(/\s\s+/g, ' '); 22 | }); 23 | } 24 | 25 | /** 26 | * @description convert string or number to integer 27 | * @example 28 | * @IsNumber() 29 | * @ToInt() 30 | * name: number; 31 | * @returns {(target: any, key: string) => void} 32 | * @constructor 33 | */ 34 | export function ToInt() { 35 | return Transform(value => parseInt(value, 10), { toClassOnly: true }); 36 | } 37 | 38 | /** 39 | * @description transforms to array, specially for query params 40 | * @example 41 | * @IsNumber() 42 | * @ToArray() 43 | * name: number; 44 | * @constructor 45 | */ 46 | export function ToArray(): (target: any, key: string) => void { 47 | return Transform( 48 | value => { 49 | if (_.isNil(value)) { 50 | return []; 51 | } 52 | return _.castArray(value); 53 | }, 54 | { toClassOnly: true }, 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/decorators/validators.decorator.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:naming-convention */ 2 | 3 | import { 4 | registerDecorator, 5 | ValidationOptions, 6 | ValidationArguments, 7 | } from 'class-validator'; 8 | 9 | export function IsPassword( 10 | validationOptions?: ValidationOptions, 11 | ): PropertyDecorator { 12 | return (object: any, propertyName: string) => { 13 | registerDecorator({ 14 | propertyName, 15 | name: 'isPassword', 16 | target: object.constructor, 17 | constraints: [], 18 | options: validationOptions, 19 | validator: { 20 | validate(value: string, _args: ValidationArguments) { 21 | return /^[a-zA-Z0-9!@#$%^&*]*$/.test(value); 22 | }, 23 | }, 24 | }); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/exceptions/file-not-image.exception.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { BadRequestException } from '@nestjs/common'; 4 | 5 | export class FileNotImageException extends BadRequestException { 6 | constructor(message?: string | object | any, error?: string) { 7 | if (message) { 8 | super(message, error); 9 | } else { 10 | super('error.file.not_image'); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/exceptions/user-not-found.exception.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { NotFoundException } from '@nestjs/common'; 4 | 5 | export class UserNotFoundException extends NotFoundException { 6 | constructor(error?: string) { 7 | super('error.user_not_found', error); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/filters/bad-request.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | BadRequestException, 6 | HttpStatus, 7 | } from '@nestjs/common'; 8 | import { STATUS_CODES } from 'http'; 9 | import { Reflector } from '@nestjs/core'; 10 | import { ValidationError } from 'class-validator'; 11 | import { Response } from 'express'; 12 | import * as _ from 'lodash'; 13 | 14 | @Catch(BadRequestException) 15 | export class HttpExceptionFilter implements ExceptionFilter { 16 | constructor(public reflector: Reflector) {} 17 | 18 | catch(exception: BadRequestException, host: ArgumentsHost) { 19 | const ctx = host.switchToHttp(); 20 | const response = ctx.getResponse(); 21 | let statusCode = exception.getStatus(); 22 | const r = exception.getResponse(); 23 | 24 | if (_.isArray(r.message) && r.message[0] instanceof ValidationError) { 25 | statusCode = HttpStatus.UNPROCESSABLE_ENTITY; 26 | const validationErrors = r.message; 27 | this._validationFilter(validationErrors); 28 | } 29 | 30 | r.statusCode = statusCode; 31 | r.error = STATUS_CODES[statusCode]; 32 | 33 | response.status(statusCode).json(r); 34 | } 35 | 36 | private _validationFilter(validationErrors: ValidationError[]) { 37 | for (const validationError of validationErrors) { 38 | for (const [constraintKey, constraint] of Object.entries( 39 | validationError.constraints, 40 | )) { 41 | if (!constraint) { 42 | // convert error message to error.fields.{key} syntax for i18n translation 43 | validationError.constraints[constraintKey] = 44 | 'error.fields.' + _.snakeCase(constraintKey); 45 | } 46 | } 47 | if (!_.isEmpty(validationError.children)) { 48 | this._validationFilter(validationError.children); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/filters/constraint-errors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | interface IConstraintErrors { 4 | [constraintKey: string]: string; 5 | } 6 | 7 | export const ConstraintErrors: IConstraintErrors = { 8 | UQ_97672ac88f789774dd47f7c8be3: 'error.unique.email', 9 | }; 10 | -------------------------------------------------------------------------------- /src/filters/query-failed.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | HttpStatus, 6 | } from '@nestjs/common'; 7 | import { Reflector } from '@nestjs/core'; 8 | import { Response } from 'express'; 9 | import { STATUS_CODES } from 'http'; 10 | import { QueryFailedError } from 'typeorm'; 11 | 12 | import { ConstraintErrors } from './constraint-errors'; 13 | 14 | @Catch(QueryFailedError) 15 | export class QueryFailedFilter implements ExceptionFilter { 16 | constructor(public reflector: Reflector) {} 17 | 18 | catch(exception: any, host: ArgumentsHost) { 19 | const ctx = host.switchToHttp(); 20 | const response = ctx.getResponse(); 21 | 22 | const errorMessage = ConstraintErrors[exception.constraint]; 23 | 24 | const status = 25 | exception.constraint && exception.constraint.startsWith('UQ') 26 | ? HttpStatus.CONFLICT 27 | : HttpStatus.INTERNAL_SERVER_ERROR; 28 | 29 | response.status(status).json({ 30 | statusCode: status, 31 | error: STATUS_CODES[status], 32 | message: errorMessage, 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard as NestAuthGuard } from '@nestjs/passport'; 2 | 3 | export const AuthGuard = NestAuthGuard('jwt'); 4 | -------------------------------------------------------------------------------- /src/guards/roles.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { UserEntity } from '../modules/user/user.entity'; 4 | 5 | @Injectable() 6 | export class RolesGuard implements CanActivate { 7 | constructor(private readonly _reflector: Reflector) {} 8 | 9 | canActivate(context: ExecutionContext): boolean { 10 | const roles = this._reflector.get( 11 | 'roles', 12 | context.getHandler(), 13 | ); 14 | 15 | if (!roles) { 16 | return true; 17 | } 18 | 19 | const request = context.switchToHttp().getRequest(); 20 | const user = request.user; 21 | 22 | return roles.includes(user.role); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/interceptors/auth-user-interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | CallHandler, 7 | } from '@nestjs/common'; 8 | 9 | import { UserEntity } from '../modules/user/user.entity'; 10 | import { AuthService } from '../modules/auth/auth.service'; 11 | 12 | @Injectable() 13 | export class AuthUserInterceptor implements NestInterceptor { 14 | intercept(context: ExecutionContext, next: CallHandler): Observable { 15 | const request = context.switchToHttp().getRequest(); 16 | 17 | const user = request.user; 18 | AuthService.setAuthUser(user); 19 | 20 | return next.handle(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/interfaces/IAwsConfig.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export interface IAwsConfig { 4 | accessKeyId: string; 5 | secretAccessKey: string; 6 | bucketName: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/IFile.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export interface IFile { 4 | encoding: string; 5 | buffer: Buffer; 6 | fieldname: string; 7 | mimetype: string; 8 | originalname: string; 9 | size: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/main.hmr.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe, ClassSerializerInterceptor } from '@nestjs/common'; 2 | import { NestFactory, Reflector } from '@nestjs/core'; 3 | import { Transport } from '@nestjs/microservices'; 4 | import { NestExpressApplication } from '@nestjs/platform-express'; 5 | import * as compression from 'compression'; 6 | import * as RateLimit from 'express-rate-limit'; 7 | import * as helmet from 'helmet'; 8 | import * as morgan from 'morgan'; 9 | 10 | import { AppModule } from './app.module'; 11 | import { HttpExceptionFilter } from './filters/bad-request.filter'; 12 | import { ConfigService } from './shared/services/config.service'; 13 | import { SharedModule } from './shared/shared.module'; 14 | import { setupSwagger } from './viveo-swagger'; 15 | 16 | declare const module: any; 17 | 18 | async function bootstrap() { 19 | const app = await NestFactory.create(AppModule, { 20 | cors: true, 21 | bodyParser: true, 22 | }); 23 | app.enable('trust proxy'); // only if you're behind a reverse proxy (Heroku, Bluemix, AWS ELB, Nginx, etc) 24 | app.use(helmet()); 25 | app.use( 26 | new RateLimit({ 27 | windowMs: 15 * 60 * 1000, // 15 minutes 28 | max: 100, // limit each IP to 100 requests per windowMs 29 | }), 30 | ); 31 | app.use(compression()); 32 | app.use(morgan('combined')); 33 | 34 | const reflector = app.get(Reflector); 35 | 36 | app.useGlobalFilters(new HttpExceptionFilter(reflector)); 37 | 38 | app.useGlobalInterceptors(new ClassSerializerInterceptor(reflector)); 39 | 40 | app.useGlobalPipes( 41 | new ValidationPipe({ 42 | whitelist: true, 43 | transform: true, 44 | dismissDefaultMessages: true, 45 | validationError: { 46 | target: false, 47 | }, 48 | }), 49 | ); 50 | 51 | const configService = app.select(SharedModule).get(ConfigService); 52 | 53 | app.connectMicroservice({ 54 | transport: Transport.TCP, 55 | options: { 56 | port: configService.getNumber('TRANSPORT_PORT'), 57 | retryAttempts: 5, 58 | retryDelay: 3000, 59 | }, 60 | }); 61 | 62 | await app.startAllMicroservicesAsync(); 63 | 64 | if (['development', 'staging'].includes(configService.nodeEnv)) { 65 | setupSwagger(app); 66 | } 67 | 68 | const port = configService.getNumber('PORT'); 69 | await app.listen(port); 70 | 71 | console.info(`server running on port ${port}`); 72 | 73 | if (module.hot) { 74 | module.hot.accept(); 75 | module.hot.dispose(() => app.close()); 76 | } 77 | } 78 | 79 | bootstrap(); 80 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {ValidationPipe, ClassSerializerInterceptor} from '@nestjs/common'; 2 | import {NestFactory, Reflector} from '@nestjs/core'; 3 | import {Transport} from '@nestjs/microservices'; 4 | import { 5 | NestExpressApplication, 6 | ExpressAdapter, 7 | } from '@nestjs/platform-express'; 8 | import * as compression from 'compression'; 9 | import * as RateLimit from 'express-rate-limit'; 10 | import * as helmet from 'helmet'; 11 | import * as morgan from 'morgan'; 12 | import { 13 | initializeTransactionalContext, 14 | patchTypeORMRepositoryWithBaseRepository, 15 | } from 'typeorm-transactional-cls-hooked'; 16 | 17 | import {AppModule} from './app.module'; 18 | import {HttpExceptionFilter} from './filters/bad-request.filter'; 19 | import {QueryFailedFilter} from './filters/query-failed.filter'; 20 | import {ConfigService} from './shared/services/config.service'; 21 | import {SharedModule} from './shared/shared.module'; 22 | import {setupSwagger} from './viveo-swagger'; 23 | import {IoAdapter} from "@nestjs/platform-socket.io"; 24 | import * as redisIoAdapter from 'socket.io-redis'; 25 | 26 | const redisAdapter = redisIoAdapter({host: 'localhost', port: 6379}); 27 | 28 | export class RedisIoAdapter extends IoAdapter { 29 | createIOServer(port: number, options?: any): any { 30 | const server = super.createIOServer(port, options); 31 | server.adapter(redisAdapter); 32 | return server; 33 | } 34 | } 35 | 36 | async function bootstrap() { 37 | initializeTransactionalContext(); 38 | patchTypeORMRepositoryWithBaseRepository(); 39 | const app = await NestFactory.create( 40 | AppModule, 41 | new ExpressAdapter(), 42 | {cors: true}, 43 | ); 44 | 45 | app.useWebSocketAdapter(new RedisIoAdapter(app)); 46 | 47 | // app.enable('trust proxy'); // only if you're behind a reverse proxy (Heroku, Bluemix, AWS ELB, Nginx, etc) 48 | app.use(helmet()); 49 | app.use( 50 | new RateLimit({ 51 | windowMs: 15 * 60 * 1000, // 15 minutes 52 | max: 100, // limit each IP to 100 requests per windowMs 53 | }), 54 | ); 55 | app.use(compression()); 56 | app.use(morgan('combined')); 57 | 58 | const reflector = app.get(Reflector); 59 | 60 | app.useGlobalFilters( 61 | new HttpExceptionFilter(reflector), 62 | new QueryFailedFilter(reflector), 63 | ); 64 | 65 | app.useGlobalInterceptors(new ClassSerializerInterceptor(reflector)); 66 | 67 | app.useGlobalPipes( 68 | new ValidationPipe({ 69 | whitelist: true, 70 | transform: true, 71 | dismissDefaultMessages: true, 72 | validationError: { 73 | target: false, 74 | }, 75 | }), 76 | ); 77 | 78 | const configService = app.select(SharedModule).get(ConfigService); 79 | 80 | app.connectMicroservice({ 81 | transport: Transport.TCP, 82 | options: { 83 | port: configService.getNumber('TRANSPORT_PORT'), 84 | retryAttempts: 5, 85 | retryDelay: 3000, 86 | }, 87 | }); 88 | 89 | // app.connectMicroservice({ 90 | // transport: Transport.REDIS, 91 | // options: { 92 | // url: 'redis://localhost:6379', 93 | // }, 94 | // }); 95 | 96 | await app.startAllMicroservicesAsync(); 97 | 98 | if (['development', 'staging'].includes(configService.nodeEnv)) { 99 | setupSwagger(app); 100 | } 101 | 102 | const port = configService.getNumber('PORT'); 103 | await app.listen(port); 104 | 105 | console.info(`server running on port ${port}`); 106 | } 107 | 108 | bootstrap(); 109 | -------------------------------------------------------------------------------- /src/middlewares/context.middelware.ts: -------------------------------------------------------------------------------- 1 | import * as requestContext from 'request-context'; 2 | 3 | export const contextMiddleware = requestContext.middleware('request'); 4 | -------------------------------------------------------------------------------- /src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context.middelware'; 2 | -------------------------------------------------------------------------------- /src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | Body, 5 | HttpCode, 6 | HttpStatus, 7 | Get, 8 | UseInterceptors, 9 | UseGuards, 10 | } from '@nestjs/common'; 11 | 12 | import { 13 | ApiOkResponse, 14 | ApiUseTags, 15 | ApiBearerAuth, 16 | } from '@nestjs/swagger'; 17 | 18 | import { AuthUser } from '../../decorators/auth-user.decorator'; 19 | import { AuthGuard } from '../../guards/auth.guard'; 20 | import { AuthUserInterceptor } from '../../interceptors/auth-user-interceptor.service'; 21 | import { UserDto } from '../user/dto/UserDto'; 22 | import { UserEntity } from '../user/user.entity'; 23 | import { UserService } from '../user/user.service'; 24 | import { AuthService } from './auth.service'; 25 | import { LoginPayloadDto } from './dto/LoginPayloadDto'; 26 | import { UserLoginDto } from './dto/UserLoginDto'; 27 | import { UserRegisterDto } from './dto/UserRegisterDto'; 28 | 29 | @Controller('auth') 30 | @ApiUseTags('auth') 31 | export class AuthController { 32 | constructor( 33 | public readonly userService: UserService, 34 | public readonly authService: AuthService, 35 | ) {} 36 | 37 | @Post('login') 38 | @HttpCode(HttpStatus.OK) 39 | @ApiOkResponse({ 40 | type: LoginPayloadDto, 41 | description: 'User info with access token', 42 | }) 43 | async userLogin( 44 | @Body() userLoginDto: UserLoginDto, 45 | ): Promise { 46 | const userEntity = await this.authService.validateUser(userLoginDto); 47 | 48 | const token = await this.authService.createToken(userEntity); 49 | return new LoginPayloadDto(userEntity.toDto(), token); 50 | } 51 | 52 | @Post('register') 53 | @HttpCode(HttpStatus.OK) 54 | @ApiOkResponse({ type: UserDto, description: 'Successfully Registered' }) 55 | async userRegister( 56 | @Body() userRegisterDto: UserRegisterDto, 57 | ): Promise { 58 | const createdUser = await this.userService.createUser( 59 | userRegisterDto, 60 | ); 61 | 62 | return createdUser.toDto(); 63 | } 64 | 65 | @Get('me') 66 | @HttpCode(HttpStatus.OK) 67 | @UseGuards(AuthGuard) 68 | @UseInterceptors(AuthUserInterceptor) 69 | @ApiBearerAuth() 70 | @ApiOkResponse({ type: UserDto, description: 'current user info' }) 71 | getCurrentUser(@AuthUser() user: UserEntity) { 72 | return user.toDto(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, forwardRef } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | 4 | import { UserModule } from '../user/user.module'; 5 | import { AuthController } from './auth.controller'; 6 | import { AuthService } from './auth.service'; 7 | import { JwtStrategy } from './jwt.strategy'; 8 | import {TypeOrmModule} from "@nestjs/typeorm"; 9 | 10 | @Module({ 11 | imports: [ 12 | forwardRef(() => UserModule),TypeOrmModule, 13 | PassportModule.register({ defaultStrategy: 'jwt' }), 14 | ], 15 | controllers: [AuthController], 16 | providers: [AuthService, JwtStrategy], 17 | exports: [PassportModule.register({ defaultStrategy: 'jwt' }), AuthService], 18 | }) 19 | export class AuthModule {} 20 | -------------------------------------------------------------------------------- /src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import {JwtService} from '@nestjs/jwt'; 2 | import {Injectable, UnauthorizedException} from '@nestjs/common'; 3 | 4 | import {ConfigService} from '../../shared/services/config.service'; 5 | import {UserEntity} from '../user/user.entity'; 6 | import {UserLoginDto} from './dto/UserLoginDto'; 7 | import {UserNotFoundException} from '../../exceptions/user-not-found.exception'; 8 | import {UtilsService} from '../../providers/utils.service'; 9 | import {UserService} from '../user/user.service'; 10 | import {UserDto} from '../user/dto/UserDto'; 11 | import {ContextService} from '../../providers/context.service'; 12 | import {TokenPayloadDto} from './dto/TokenPayloadDto'; 13 | import {InjectRepository} from "@nestjs/typeorm"; 14 | import {UserRepository} from "../user/user.repository"; 15 | import {Socket} from "socket.io"; 16 | 17 | @Injectable() 18 | export class AuthService { 19 | private static _authUserKey = 'user_key'; 20 | 21 | constructor( 22 | public readonly jwtService: JwtService, 23 | public readonly configService: ConfigService, 24 | public readonly userService: UserService, 25 | @InjectRepository(UserRepository) 26 | public readonly userRepository: UserRepository, 27 | ) { 28 | } 29 | 30 | async createToken(user: UserEntity | UserDto): Promise { 31 | return new TokenPayloadDto({ 32 | expiresIn: this.configService.getNumber('JWT_EXPIRATION_TIME'), 33 | accessToken: await this.jwtService.signAsync({id: user.id}), 34 | }); 35 | } 36 | 37 | async validateUser(userLoginDto: UserLoginDto): Promise { 38 | const user = await this.userService.findOne({ 39 | email: userLoginDto.email, 40 | }); 41 | const isPasswordValid = await UtilsService.validateHash( 42 | userLoginDto.password, 43 | user && user.password, 44 | ); 45 | if (!user || !isPasswordValid) { 46 | throw new UserNotFoundException(); 47 | } 48 | return user; 49 | } 50 | 51 | static setAuthUser(user: UserEntity) { 52 | ContextService.set(AuthService._authUserKey, user); 53 | } 54 | 55 | static getAuthUser(): UserEntity { 56 | return ContextService.get(AuthService._authUserKey); 57 | } 58 | 59 | /* 60 | * login user on socket, set user on client request 61 | * */ 62 | async loginSocket(client: Socket): Promise { 63 | const {iat, exp, id: userId} = client.request.decoded_token; 64 | 65 | const timeDiff = exp - iat; 66 | if (timeDiff <= 0) { 67 | throw new UnauthorizedException(); 68 | // return false; 69 | } 70 | const user = await this.userRepository.findOne(userId, {relations: ['rooms']}); 71 | 72 | if (!user) { 73 | throw new UnauthorizedException(); 74 | // return false; 75 | } 76 | 77 | // set user on client request for another handlers to get authenticated user. 78 | client.request.user = user; 79 | return user 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/modules/auth/dto/LoginPayloadDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { TokenPayloadDto } from './TokenPayloadDto'; 4 | import { UserDto } from '../../user/dto/UserDto'; 5 | import { ApiModelProperty } from '@nestjs/swagger'; 6 | 7 | export class LoginPayloadDto { 8 | @ApiModelProperty({ type: UserDto }) 9 | user: UserDto; 10 | @ApiModelProperty({ type: TokenPayloadDto }) 11 | token: TokenPayloadDto; 12 | 13 | constructor(user: UserDto, token: TokenPayloadDto) { 14 | this.user = user; 15 | this.token = token; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/auth/dto/TokenPayloadDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { ApiModelProperty } from '@nestjs/swagger'; 4 | 5 | export class TokenPayloadDto { 6 | @ApiModelProperty() 7 | expiresIn: number; 8 | 9 | @ApiModelProperty() 10 | accessToken: string; 11 | 12 | constructor(data: { expiresIn: number; accessToken: string }) { 13 | this.expiresIn = data.expiresIn; 14 | this.accessToken = data.accessToken; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/auth/dto/UserLoginDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { IsString, IsEmail } from 'class-validator'; 4 | import { ApiModelProperty } from '@nestjs/swagger'; 5 | 6 | export class UserLoginDto { 7 | @IsString() 8 | @IsEmail() 9 | @ApiModelProperty() 10 | readonly email: string; 11 | 12 | @IsString() 13 | @ApiModelProperty() 14 | readonly password: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/auth/dto/UserRegisterDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | IsString, 5 | IsEmail, 6 | MinLength, 7 | IsNotEmpty, 8 | IsPhoneNumber, 9 | IsOptional, 10 | } from 'class-validator'; 11 | import { ApiModelProperty } from '@nestjs/swagger'; 12 | import { Column } from 'typeorm'; 13 | 14 | export class UserRegisterDto { 15 | @IsString() 16 | @IsNotEmpty() 17 | @ApiModelProperty() 18 | readonly firstName: string; 19 | 20 | @IsString() 21 | @IsNotEmpty() 22 | @ApiModelProperty() 23 | readonly lastName: string; 24 | 25 | @IsString() 26 | @IsEmail() 27 | @IsNotEmpty() 28 | @ApiModelProperty() 29 | readonly email: string; 30 | 31 | @IsString() 32 | @MinLength(6) 33 | @ApiModelProperty({ minLength: 6 }) 34 | readonly password: string; 35 | 36 | @Column() 37 | @IsPhoneNumber('ZZ') 38 | @IsOptional() 39 | @ApiModelProperty() 40 | phone: string; 41 | } 42 | -------------------------------------------------------------------------------- /src/modules/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | 5 | import { ConfigService } from '../../shared/services/config.service'; 6 | import { UserService } from '../user/user.service'; 7 | 8 | @Injectable() 9 | export class JwtStrategy extends PassportStrategy(Strategy) { 10 | constructor( 11 | public readonly configService: ConfigService, 12 | public readonly userService: UserService, 13 | ) { 14 | super({ 15 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 16 | secretOrKey: configService.get('JWT_SECRET_KEY'), 17 | }); 18 | } 19 | 20 | async validate({ iat, exp, id: userId }) { 21 | const timeDiff = exp - iat; 22 | if (timeDiff <= 0) { 23 | throw new UnauthorizedException(); 24 | } 25 | const user = await this.userService.findOne(userId); 26 | 27 | if (!user) { 28 | throw new UnauthorizedException(); 29 | } 30 | return user; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/chat/chat.controller.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {Body, Controller, HttpCode, HttpStatus, Post, UseGuards, UseInterceptors,} from '@nestjs/common'; 4 | import {ApiBearerAuth, ApiUseTags} from '@nestjs/swagger'; 5 | import {ChatService} from './chat.service'; 6 | import {CreateMessageDto} from "./dto/createMessageDto"; 7 | import {AuthUserInterceptor} from "../../interceptors/auth-user-interceptor.service"; 8 | import {AuthGuard} from "../../guards/auth.guard"; 9 | import {RolesGuard} from "../../guards/roles.guard"; 10 | import {AuthUser} from "../../decorators/auth-user.decorator"; 11 | import {UserEntity} from "../user/user.entity"; 12 | import {UserDto} from "../user/dto/UserDto"; 13 | 14 | @Controller('chat') 15 | @ApiUseTags('chat') 16 | @UseGuards(AuthGuard, RolesGuard) 17 | @UseInterceptors(AuthUserInterceptor) 18 | @ApiBearerAuth() 19 | export class ChatController { 20 | constructor(private _chatService: ChatService) {} 21 | 22 | 23 | 24 | // @Post('new_msg') 25 | // @HttpCode(HttpStatus.OK) 26 | // // @ApiOkResponse({ type: MessageDto, description: 'Successfully Registered' }) 27 | // async createMessage(@AuthUser() user: UserEntity, 28 | // @Body() messageDto: CreateMessageDto, 29 | // ): Promise { 30 | // 31 | // console.log(user); 32 | // const createdUser = await this._chatService.createMessage( 33 | // messageDto, user 34 | // ); 35 | // 36 | // return user.toDto(); 37 | // } 38 | 39 | // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImM4YmFjMjBhLWQxMDMtNDM5Yi1iZWEwLWYwZjMxMjlkZDJhZCIsImlhdCI6MTU4MTUxMDcyM30.B8MYG1VeGW9yNCDrlqSe9dLrkARNlhAzL42qAeoUzvU 40 | /* 41 | * { 42 | "text": "string", 43 | "sender":"c8bac20a-d103-439b-bea0-f0f3129dd2ad", 44 | "receiver":"abe95179-2ffb-4fd4-bbfa-e69e08e4e450" 45 | 46 | } 47 | * */ 48 | 49 | // @Get('admin') 50 | // @Roles(RoleType.USER) 51 | // @HttpCode(HttpStatus.OK) 52 | // async admin(@AuthUser() user: UserEntity) { 53 | // return 'only for you admin: ' + user.firstName; 54 | // } 55 | // 56 | // @Get('users') 57 | // @Roles(RoleType.ADMIN) 58 | // @HttpCode(HttpStatus.OK) 59 | // @ApiResponse({ 60 | // status: HttpStatus.OK, 61 | // description: 'Get users list', 62 | // type: UsersPageDto, 63 | // }) 64 | // getUsers( 65 | // @Query(new ValidationPipe({ transform: true })) 66 | // pageOptionsDto: UsersPageOptionsDto, 67 | // ): Promise { 68 | // return this._userService.getUsers(pageOptionsDto); 69 | // } 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/chat/chat.gateway.ts: -------------------------------------------------------------------------------- 1 | import {Logger, OnModuleInit, UnauthorizedException, UsePipes, ValidationPipe,} from '@nestjs/common'; 2 | import { 3 | ConnectedSocket, 4 | MessageBody, 5 | OnGatewayConnection, 6 | OnGatewayDisconnect, 7 | OnGatewayInit, 8 | SubscribeMessage, 9 | WebSocketGateway, 10 | WebSocketServer, 11 | } from '@nestjs/websockets'; 12 | import {Server, Socket} from 'socket.io'; 13 | import * as socketioJwt from 'socketio-jwt'; 14 | 15 | import {UserService} from '../user/user.service'; 16 | import {ConfigService} from '../../shared/services/config.service'; 17 | import {ChatService} from "./chat.service"; 18 | import {CreateMessageDto} from "./dto/createMessageDto"; 19 | import {RedisService} from "nestjs-redis"; 20 | import {CreateRoomDto} from "./dto/createRoomDto"; 21 | import {CreatePrivateMessageDto} from "./dto/createPrivateMessageDto"; 22 | import {InjectRepository} from "@nestjs/typeorm"; 23 | import {UserRepository} from "../user/user.repository"; 24 | import {RoomRepository} from "./room.repository"; 25 | import {UtilsService} from "../../providers/utils.service"; 26 | import {AuthService} from "../auth/auth.service"; 27 | import {RoomEntity} from "./room.entity"; 28 | import {JoinRoomDto} from "./dto/joinRoomDto"; 29 | 30 | 31 | // import {JwtGuard} from "../auth/wsjwt.guard"; 32 | @UsePipes(ValidationPipe) 33 | @WebSocketGateway() 34 | export class ChatGateway 35 | implements OnGatewayInit, 36 | OnGatewayConnection, 37 | OnGatewayDisconnect, 38 | OnModuleInit { 39 | constructor( 40 | public readonly configService: ConfigService, 41 | public readonly userService: UserService, 42 | private _chatService: ChatService, 43 | private readonly redisService: RedisService, 44 | private authService: AuthService, 45 | @InjectRepository(UserRepository) 46 | public readonly userRepository: UserRepository, 47 | @InjectRepository(RoomRepository) 48 | public readonly roomRepository: RoomRepository, 49 | ) { 50 | } 51 | 52 | // @Client({ 53 | // transport: Transport.REDIS, options: { 54 | // url: 'redis://localhost:6379', 55 | // } 56 | // }) 57 | // redisClient: ClientProxy; 58 | 59 | 60 | @WebSocketServer() 61 | server: Server; 62 | 63 | onModuleInit() { 64 | (this.server).set( 65 | 'authorization', 66 | socketioJwt.authorize({ 67 | secret: this.configService.get('JWT_SECRET_KEY'), 68 | handshake: true, 69 | }), 70 | ); 71 | } 72 | 73 | private logger: Logger = new Logger('AppGateway'); 74 | 75 | // @UseGuards(WsAuthGuard) 76 | // @SubscribeMessage('msgToServer') 77 | // async handleMessage(client: Socket, payload: CreateMessageDto) { 78 | // // const {user, ...result} = payload; 79 | // // console.log(client.request.user); 80 | // const createdMessage = await this._chatService.createMessage(payload, client.request.user); 81 | // // this.logger.log(payload, 'msgToServer'); 82 | // const ans = {"name": client.request.user.email, "text": payload.text}; 83 | // // console.log(this.server.clients().sockets); 84 | // // const tt = await this.redisService.getClient().get(`users:${client.request.user.id}`); 85 | // // const tt = await this.redisService.getClient().get(`users:${payload.receiver}`); 86 | // // console.log(tt); 87 | // // if (tt) { 88 | // // this.server.emit('msgToClient', ans); 89 | // // this.server.to(createdMessage.room.name).emit('msgToClient', ans); 90 | // // } 91 | // } 92 | 93 | @SubscribeMessage('createNewPublicRoom') 94 | async handleCreatePublicRoom(client: Socket, payload: CreateRoomDto) { 95 | 96 | const exists: RoomEntity = await this.roomRepository.findOne({where: {name: payload.name}}); 97 | if (exists) { 98 | console.log('room exists already with this name'); 99 | return; 100 | } 101 | const room: RoomEntity = await this.roomRepository.save({ 102 | name: payload.name, 103 | isPrivate: false, 104 | members: [client.request.user] 105 | }); 106 | client.join(room.name); 107 | 108 | const answerPayload = {"name": room.name, "text": 'new room created'}; 109 | 110 | this.server.to(room.name).emit('createdNewPublicRoom', answerPayload); 111 | } 112 | 113 | 114 | @SubscribeMessage('joinPublicRoom') 115 | async handleJoinRoom(@ConnectedSocket() client: Socket, @MessageBody() payload: JoinRoomDto) { 116 | const room: RoomEntity = await this.roomRepository.findOne({name: payload.name}, {relations: ['members']}); 117 | if (!room) { 118 | console.log('room not found'); 119 | return; 120 | } 121 | 122 | let isJoined = await this.roomRepository.join(room, client.request.user); 123 | if (!isJoined) { 124 | client.emit('not joined'); 125 | } 126 | client.join(room.name); 127 | 128 | const answerPayload = {"name": client.request.user.email, "text": 'new user joined'}; 129 | 130 | this.server.to(room.name).emit('userJoined', answerPayload); 131 | } 132 | 133 | @SubscribeMessage('getRooms') 134 | async handleGetRooms(@ConnectedSocket() client: Socket, payload) { 135 | const pvrooms: RoomEntity[] = await this.roomRepository.find({ 136 | where: {isPrivate: true}, 137 | relations: ['members'] 138 | },); 139 | const pubrooms: RoomEntity[] = await this.roomRepository.find({where: {isPrivate: false}}); 140 | pvrooms.forEach(value => { 141 | if (value.isPrivate) { 142 | value.name = value.members[0].email 143 | } 144 | }); 145 | // console.log(rooms); 146 | client.emit('getRooms', [...pvrooms, ...pubrooms]); 147 | } 148 | 149 | 150 | @SubscribeMessage('msgToRoomServer') 151 | async handleRoomMessage(client: Socket, payload: CreateMessageDto) { 152 | const room = await this.roomRepository.findOne({ 153 | where: {name: payload.room_name, isPrivate: false}, 154 | relations: ['members'] 155 | }); 156 | 157 | if (!room) { 158 | this.logger.log('room not found'); 159 | return; 160 | } 161 | const createdMessage = await this._chatService.createPublicRoomMessage(client.request.user, room, payload.text); 162 | const answerPayload = {"name": client.request.user.email, "text": payload.text}; 163 | this.server.to(createdMessage.room.name).emit('msgToRoomClient', answerPayload); 164 | } 165 | 166 | 167 | @SubscribeMessage('msgPrivateToServer') 168 | async handlePrivateMessage(client: Socket, payload: CreatePrivateMessageDto) { 169 | const receiver = await this.userService.findOne({id: payload.receiver}); 170 | // 171 | if (!receiver) { 172 | console.log('receiver not found'); 173 | return; 174 | } 175 | const createdMessage = await this._chatService.createPrivateMessage(client.request.user, receiver, payload.text); 176 | 177 | const answerPayload = {"name": client.request.user.email, "text": payload.text}; 178 | const receiverSocketId: string = await this.redisService.getClient().get(`users:${payload.receiver}`); 179 | 180 | // join two clients to room 181 | const receiverSocketObject = this.server.clients().sockets[receiverSocketId]; 182 | receiverSocketObject.join(createdMessage.room.name); 183 | client.join(createdMessage.room.name); 184 | 185 | // if receiver is online 186 | if (receiverSocketId) { 187 | this.server.to(createdMessage.room.name).emit('msgPrivateToClient', answerPayload); 188 | } 189 | } 190 | 191 | 192 | afterInit(server: Server) { 193 | this.logger.log('Init'); 194 | } 195 | 196 | async handleDisconnect(client: Socket) { 197 | await this.redisService.getClient().del(`users:${client.request.user.id}`); 198 | this.logger.log(`Client disconnected: ${client.id}`); 199 | } 200 | 201 | async handleConnection(client: Socket, ...args: any[]) { 202 | const user = await this.authService.loginSocket(client); 203 | 204 | // set on redis=> key: user.id, value: socketId 205 | await UtilsService.setUserIdAndSocketIdOnRedis(this.redisService, client.request.user.id, client.id); 206 | // join to all user's room, so can get sent messages immediately 207 | this.roomRepository.initJoin(user, client); 208 | 209 | this.logger.log(`Client connected: ${client.id}`); 210 | 211 | } 212 | 213 | 214 | // @SubscribeMessage('createRoom') 215 | // async createRoom(client: Socket, payload: CreateRoomDto) { 216 | // const room = await this._chatService.createRoom(payload, client.request.user); 217 | // } 218 | 219 | 220 | // @SubscribeMessage('joinRoom') 221 | // async joinRoom(client: Socket, payload: CreateRoomDto) { 222 | // await this._chatService.joinRoom(payload, client.request.user); 223 | // } 224 | 225 | // -------------------------------------------- before ------------------------------------------------ 226 | // @SubscribeMessage('message') 227 | // handleMessage(client: any, payload: any): string { 228 | // console.log(client); 229 | // console.log(payload); 230 | // return 'Hello world!'; 231 | // } 232 | // 233 | // // better for testing 234 | // @SubscribeMessage('events') 235 | // handleEvent(@MessageBody() data: string, @ConnectedSocket() client: Socket) { 236 | // client.emit('res', data); 237 | // // return data; 238 | // } 239 | // 240 | // @SubscribeMessage('events') 241 | // handlePM(@MessageBody() data: unknown): WsResponse { 242 | // // client.emit('res', data); 243 | // const event = 'PM'; 244 | // return {event, data} 245 | // 246 | // // return data; 247 | // } 248 | } 249 | -------------------------------------------------------------------------------- /src/modules/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import {forwardRef, Module} from '@nestjs/common'; 2 | 3 | import {UserModule} from '../user/user.module'; 4 | import {ChatGateway} from './chat.gateway'; 5 | import {ChatController} from './chat.controller'; 6 | import {TypeOrmModule} from "@nestjs/typeorm"; 7 | import {ChatService} from "./chat.service"; 8 | import {MessageRepository} from "./message.repository"; 9 | import {RoomRepository} from "./room.repository"; 10 | import {AuthModule} from "../auth/auth.module"; 11 | 12 | @Module({ 13 | providers: [ChatGateway, ChatService], 14 | imports: [ 15 | TypeOrmModule.forFeature([MessageRepository, RoomRepository]), 16 | forwardRef(() => UserModule), 17 | AuthModule, 18 | 19 | ], 20 | controllers: [ChatController], 21 | exports: [TypeOrmModule] 22 | }) 23 | export class ChatModule { 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/chat/chat.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {MessageRepository} from "./message.repository"; 3 | import {InjectRepository} from "@nestjs/typeorm"; 4 | import {UserRepository} from "../user/user.repository"; 5 | import {CreateMessageDto} from "./dto/createMessageDto"; 6 | import {RoomRepository} from "./room.repository"; 7 | import {MessageEntity} from "./message.entity"; 8 | import {UserEntity} from "../user/user.entity"; 9 | import {RoomEntity} from "./room.entity"; 10 | 11 | @Injectable() 12 | export class ChatService { 13 | constructor( 14 | @InjectRepository(MessageRepository) 15 | public readonly messageRepository: MessageRepository, 16 | @InjectRepository(UserRepository) 17 | public readonly userRepository: UserRepository, 18 | @InjectRepository(RoomRepository) 19 | public readonly roomRepository: RoomRepository, 20 | ) { 21 | } 22 | 23 | // deprecated 24 | // async createMessage(msg: CreateMessageDto, sender): Promise { 25 | // let created_msg = this.messageRepository.create(msg); 26 | // created_msg.sender = sender; 27 | // return this.messageRepository.save(created_msg); 28 | // 29 | // } 30 | 31 | /* 32 | * check room if not exists create it, and save the message in room. 33 | */ 34 | async createPrivateMessage(sender: UserEntity, receiver: UserEntity, msg: string): Promise { 35 | let room = await this.roomRepository.checkPrivateRoomExists(sender, receiver); 36 | if (!room) { 37 | room = await this.roomRepository.createPrivateRoom(sender, receiver) 38 | } 39 | return this.messageRepository.save({text: msg, room: room, sender: sender}); 40 | } 41 | 42 | /* 43 | * check if user is joined the room before, if yes then save the message in room. 44 | */ 45 | async createPublicRoomMessage(sender: UserEntity, room: RoomEntity, msg: string): Promise { 46 | let alreadyInRoom = room.members.some(ele => ele.id === sender.id); 47 | 48 | if (!alreadyInRoom) { 49 | return; 50 | } 51 | return this.messageRepository.save({text: msg, room: room, sender: sender}); 52 | } 53 | 54 | // async createRoom(data: CreateRoomDto, sender): Promise { 55 | // const createdRoom = await this.roomRepository.createPublicRoom(data, sender); 56 | // return createdRoom.toDto() 57 | // } 58 | 59 | 60 | // async createPrivateRoom(data: CreatePrivateRoomDto): Promise { 61 | // const createdRoom = await this.roomRepository.createPrivateRoom(data.sender, data.receiver); 62 | // return createdRoom.toDto() 63 | // } 64 | 65 | // async joinRoom(room: RoomEntity, user: UserEntity): Promise { 66 | // return await this.roomRepository.join(room, user); 67 | // } 68 | 69 | 70 | // /** 71 | // * Find single user 72 | // */ 73 | // findOne(findData: FindConditions): Promise { 74 | // return this.userRepository.findOne(findData); 75 | // } 76 | // async findByUsernameOrEmail( 77 | // options: Partial<{ username: string; email: string }>, 78 | // ): Promise { 79 | // const queryBuilder = this.userRepository.createQueryBuilder('user'); 80 | // 81 | // if (options.email) { 82 | // queryBuilder.orWhere('user.email = :email', { 83 | // email: options.email, 84 | // }); 85 | // } 86 | // if (options.username) { 87 | // queryBuilder.orWhere('user.username = :username', { 88 | // username: options.username, 89 | // }); 90 | // } 91 | // 92 | // return queryBuilder.getOne(); 93 | // } 94 | // 95 | // async createUser( 96 | // userRegisterDto: UserRegisterDto, 97 | // ): Promise { 98 | // const user = this.userRepository.create({ ...userRegisterDto }); 99 | // return this.userRepository.save(user); 100 | // } 101 | // 102 | // async getUsers(pageOptionsDto: UsersPageOptionsDto): Promise { 103 | // const queryBuilder = this.userRepository.createQueryBuilder('user'); 104 | // const [users, usersCount] = await queryBuilder 105 | // .skip(pageOptionsDto.skip) 106 | // .take(pageOptionsDto.take) 107 | // .getManyAndCount(); 108 | // 109 | // const pageMetaDto = new PageMetaDto({ 110 | // pageOptionsDto, 111 | // itemCount: usersCount, 112 | // }); 113 | // return new UsersPageDto(users.toDtos(), pageMetaDto); 114 | // } 115 | } 116 | -------------------------------------------------------------------------------- /src/modules/chat/dto/MessageDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {ApiModelProperty} from '@nestjs/swagger'; 4 | import {AbstractDto} from '../../../common/dto/AbstractDto'; 5 | import {MessageEntity} from "../message.entity"; 6 | 7 | export class MessageDto extends AbstractDto { 8 | @ApiModelProperty() 9 | text: string; 10 | 11 | 12 | constructor(msg: MessageEntity) { 13 | super(msg); 14 | this.text = msg.text; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/chat/dto/RoomDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {ApiModelProperty} from '@nestjs/swagger'; 4 | import {AbstractDto} from '../../../common/dto/AbstractDto'; 5 | import {MessageEntity} from "../message.entity"; 6 | import {RoomEntity} from "../room.entity"; 7 | import {UserEntity} from "../../user/user.entity"; 8 | 9 | export class RoomDto extends AbstractDto { 10 | @ApiModelProperty() 11 | name: string; 12 | 13 | @ApiModelProperty() 14 | isPrivate: boolean; 15 | 16 | 17 | @ApiModelProperty() 18 | members: UserEntity[]; 19 | 20 | 21 | @ApiModelProperty() 22 | messages: MessageEntity[]; 23 | 24 | constructor(room: RoomEntity) { 25 | super(room); 26 | this.name = room.name; 27 | this.isPrivate = room.isPrivate; 28 | this.members = room.members; 29 | this.messages = room.messages; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/chat/dto/createMessageDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {ApiModelProperty} from '@nestjs/swagger'; 4 | import {IsNotEmpty, IsString} from "class-validator"; 5 | 6 | export class CreateMessageDto { 7 | @IsString() 8 | @IsNotEmpty() 9 | @ApiModelProperty() 10 | text: string; 11 | 12 | // @IsUUID() 13 | // @IsNotEmpty() 14 | // @ApiModelProperty() 15 | // sender: UserEntity; 16 | 17 | @IsString() 18 | @IsNotEmpty() 19 | @ApiModelProperty() 20 | room_name: string; 21 | 22 | constructor(text, room) { 23 | this.text = text; 24 | this.room_name = room; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/chat/dto/createPrivateMessageDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {ApiModelProperty} from '@nestjs/swagger'; 4 | import {IsNotEmpty, IsString, IsUUID} from "class-validator"; 5 | import {RoomEntity} from "../room.entity"; 6 | 7 | export class CreatePrivateMessageDto { 8 | @IsString() 9 | @IsNotEmpty() 10 | @ApiModelProperty() 11 | text: string; 12 | 13 | @IsUUID() 14 | @ApiModelProperty() 15 | receiver: string; 16 | 17 | @IsUUID() 18 | @ApiModelProperty() 19 | room: string; 20 | 21 | constructor(text, room) { 22 | this.text = text; 23 | this.room = room; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/chat/dto/createPrivateRoomDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {ApiModelProperty} from '@nestjs/swagger'; 4 | import {IsNotEmpty} from "class-validator"; 5 | import {UserEntity} from "../../user/user.entity"; 6 | 7 | export class CreatePrivateRoomDto { 8 | 9 | @IsNotEmpty() 10 | @ApiModelProperty() 11 | sender: UserEntity; 12 | 13 | 14 | @IsNotEmpty() 15 | @ApiModelProperty() 16 | receiver: UserEntity; 17 | 18 | // @ApiModelProperty() 19 | // members: UserEntity[]; 20 | // 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/chat/dto/createRoomDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {ApiModelProperty} from '@nestjs/swagger'; 4 | import {IsNotEmpty, IsString} from "class-validator"; 5 | 6 | export class CreateRoomDto { 7 | 8 | @IsString() 9 | @IsNotEmpty() 10 | @ApiModelProperty() 11 | name: string; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/chat/dto/joinRoomDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {ApiModelProperty} from '@nestjs/swagger'; 4 | import {IsNotEmpty, IsString} from "class-validator"; 5 | 6 | export class JoinRoomDto { 7 | 8 | @IsString() 9 | @IsNotEmpty() 10 | @ApiModelProperty() 11 | name: string; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/chat/message.entity.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, ManyToOne} from 'typeorm'; 2 | 3 | import {AbstractEntity} from '../../common/abstract.entity'; 4 | import {MessageDto} from "./dto/MessageDto"; 5 | import {UserEntity} from "../user/user.entity"; 6 | import {RoomEntity} from "./room.entity"; 7 | 8 | @Entity({name: 'messages'}) 9 | export class MessageEntity extends AbstractEntity { 10 | @Column({nullable: true}) 11 | text: string; 12 | 13 | // @ManyToOne(type => UserEntity, {cascade: ['insert', 'update']}) 14 | // @ManyToOne(type => UserEntity,) 15 | // owner: UserEntity; 16 | 17 | @ManyToOne(type => UserEntity,) 18 | sender: UserEntity; 19 | 20 | // @ManyToOne(type => UserEntity, user => user.received_messages, {nullable: true}) 21 | // receiver: UserEntity; 22 | 23 | @ManyToOne(type => RoomEntity, room => room.messages) 24 | room: RoomEntity; 25 | 26 | // @ManyToOne(type => UserEntity, message => message.received_messages) 27 | // group: UserEntity; 28 | 29 | // @Column({default: true}) 30 | // isPrivate: boolean; 31 | 32 | 33 | dtoClass = MessageDto; 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/chat/message.repository.ts: -------------------------------------------------------------------------------- 1 | import {Repository} from 'typeorm'; 2 | import {EntityRepository} from 'typeorm/decorator/EntityRepository'; 3 | import {MessageEntity} from "./message.entity"; 4 | 5 | @EntityRepository(MessageEntity) 6 | export class MessageRepository extends Repository { 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/chat/room.entity.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, JoinTable, ManyToMany, OneToMany} from 'typeorm'; 2 | 3 | import {AbstractEntity} from '../../common/abstract.entity'; 4 | import {UserEntity} from "../user/user.entity"; 5 | import {MessageEntity} from "./message.entity"; 6 | import {RoomDto} from "./dto/RoomDto"; 7 | 8 | @Entity({name: 'rooms'}) 9 | export class RoomEntity extends AbstractEntity { 10 | @Column({nullable: false}) 11 | name: string; 12 | 13 | // @ManyToOne(type => UserEntity, {cascade: ['insert', 'update']}) 14 | // @ManyToOne(type => UserEntity,) 15 | // owner: UserEntity; 16 | 17 | @ManyToMany(type => UserEntity, user => user.rooms, {cascade: true}) 18 | @JoinTable() 19 | members: UserEntity[]; 20 | 21 | @OneToMany(type => MessageEntity, message => message.room) 22 | messages: MessageEntity[]; 23 | 24 | // @ManyToOne(type => UserEntity, message => message.received_messages) 25 | // group: UserEntity; 26 | 27 | @Column({default: true}) 28 | isPrivate: boolean; 29 | 30 | 31 | dtoClass = RoomDto; 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/chat/room.repository.ts: -------------------------------------------------------------------------------- 1 | import {Repository} from 'typeorm'; 2 | import {EntityRepository} from 'typeorm/decorator/EntityRepository'; 3 | import {RoomEntity} from "./room.entity"; 4 | import {UserEntity} from "../user/user.entity"; 5 | import {CreateRoomDto} from "./dto/createRoomDto"; 6 | import {CreatePrivateRoomDto} from "./dto/createPrivateRoomDto"; 7 | 8 | @EntityRepository(RoomEntity) 9 | export class RoomRepository extends Repository { 10 | 11 | initJoin(user: UserEntity, client) { 12 | // join connected user to all the rooms that is member of. 13 | 14 | let roomsToJoin = []; 15 | user.rooms.forEach(room => { 16 | return roomsToJoin.push(room.name); 17 | }); 18 | 19 | client.join(roomsToJoin); 20 | 21 | } 22 | 23 | async join(room: RoomEntity, user: UserEntity) { 24 | if (room.isPrivate && room.members.length >= 2) 25 | return false; 26 | room.members.push(user); 27 | await this.save(room); 28 | return true; 29 | } 30 | 31 | 32 | async createPublicRoom(data: CreateRoomDto, sender): Promise { 33 | let nroom = new RoomEntity(); 34 | nroom.members = [sender]; 35 | nroom.isPrivate = false; 36 | nroom.name = data.name; 37 | await this.save(nroom); 38 | return nroom 39 | } 40 | 41 | async createPrivateRoom(sender, receiver): Promise { 42 | let nroom = new RoomEntity(); 43 | nroom.members = [sender, receiver]; 44 | nroom.isPrivate = true; 45 | nroom.name = this.generatePrivateRoomName(sender, receiver); 46 | await this.save(nroom); 47 | return nroom 48 | } 49 | 50 | 51 | async checkPrivateRoomExists(sender, receiver): Promise { 52 | return await this.findOne({name: this.generatePrivateRoomName(sender, receiver)}) 53 | } 54 | 55 | generatePrivateRoomName(sender, receiver): string { 56 | if (sender.email.localeCompare(receiver.email) === -1) { 57 | // firstUsername is "<" (before) secondUsername 58 | return sender.email + '-' + receiver.email; 59 | 60 | } else if (sender.email.localeCompare(receiver.email) === 1) { 61 | // firstUsername is ">" (after) secondUsername 62 | return receiver.email + '-' + sender.email; 63 | 64 | } else { 65 | return 'falsesss' 66 | // ids are equal, should throw an error 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/math/math.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { 3 | ClientProxy, 4 | Client, 5 | Transport, 6 | MessagePattern, 7 | } from '@nestjs/microservices'; 8 | import { Observable } from 'rxjs'; 9 | 10 | @Controller('math') 11 | export class MathController { 12 | @Client({ transport: Transport.TCP, options: { port: 4000 } }) 13 | client: ClientProxy; 14 | 15 | @Get('sum') 16 | call(): Observable { 17 | const pattern = { cmd: 'sum' }; 18 | const data = [1, 2, 3, 4, 5]; 19 | return this.client.send(pattern, data); 20 | } 21 | 22 | @MessagePattern({ cmd: 'sum' }) 23 | sum(data: number[]): number { 24 | return (data || []).reduce((a, b) => a + b); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/math/math.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MathController } from './math.controller'; 3 | 4 | @Module({ 5 | controllers: [MathController], 6 | }) 7 | export class MathModule {} 8 | -------------------------------------------------------------------------------- /src/modules/user/dto/UserDto.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { ApiModelPropertyOptional } from '@nestjs/swagger'; 4 | 5 | import { RoleType } from '../../../common/constants/role-type'; 6 | import { AbstractDto } from '../../../common/dto/AbstractDto'; 7 | import { UserEntity } from '../user.entity'; 8 | 9 | export class UserDto extends AbstractDto { 10 | @ApiModelPropertyOptional() 11 | firstName: string; 12 | 13 | @ApiModelPropertyOptional() 14 | lastName: string; 15 | 16 | @ApiModelPropertyOptional() 17 | username: string; 18 | 19 | @ApiModelPropertyOptional({ enum: RoleType }) 20 | role: RoleType; 21 | 22 | @ApiModelPropertyOptional() 23 | email: string; 24 | 25 | @ApiModelPropertyOptional() 26 | phone: string; 27 | 28 | constructor(user: UserEntity) { 29 | super(user); 30 | this.firstName = user.firstName; 31 | this.lastName = user.lastName; 32 | this.role = user.role; 33 | this.email = user.email; 34 | this.phone = user.phone; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/user/dto/UsersPageDto.ts: -------------------------------------------------------------------------------- 1 | import { UserDto } from './UserDto'; 2 | import { ApiModelProperty } from '@nestjs/swagger'; 3 | import { PageMetaDto } from '../../../common/dto/PageMetaDto'; 4 | 5 | export class UsersPageDto { 6 | @ApiModelProperty({ 7 | type: UserDto, 8 | isArray: true, 9 | }) 10 | readonly data: UserDto[]; 11 | 12 | @ApiModelProperty() 13 | readonly meta: PageMetaDto; 14 | 15 | constructor(data: UserDto[], meta: PageMetaDto) { 16 | this.data = data; 17 | this.meta = meta; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/user/dto/UsersPageOptionsDto.ts: -------------------------------------------------------------------------------- 1 | import { PageOptionsDto } from '../../../common/dto/PageOptionsDto'; 2 | 3 | export class UsersPageOptionsDto extends PageOptionsDto {} 4 | -------------------------------------------------------------------------------- /src/modules/user/password.transformer.ts: -------------------------------------------------------------------------------- 1 | import { ValueTransformer } from 'typeorm'; 2 | import { UtilsService } from '../../providers/utils.service'; 3 | 4 | export class PasswordTransformer implements ValueTransformer { 5 | to(value) { 6 | return UtilsService.generateHash(value); 7 | } 8 | from(value) { 9 | return value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | Controller, 5 | Get, 6 | HttpCode, 7 | HttpStatus, 8 | Query, 9 | UseGuards, 10 | UseInterceptors, 11 | ValidationPipe, 12 | } from '@nestjs/common'; 13 | import { ApiBearerAuth, ApiResponse, ApiUseTags } from '@nestjs/swagger'; 14 | 15 | import { RoleType } from '../../common/constants/role-type'; 16 | import { AuthUser } from '../../decorators/auth-user.decorator'; 17 | import { Roles } from '../../decorators/roles.decorator'; 18 | import { AuthGuard } from '../../guards/auth.guard'; 19 | import { RolesGuard } from '../../guards/roles.guard'; 20 | import { AuthUserInterceptor } from '../../interceptors/auth-user-interceptor.service'; 21 | import { UsersPageOptionsDto } from './dto/UsersPageOptionsDto'; 22 | import { UsersPageDto } from './dto/UsersPageDto'; 23 | import { UserEntity } from './user.entity'; 24 | import { UserService } from './user.service'; 25 | 26 | @Controller('users') 27 | @ApiUseTags('users') 28 | @UseGuards(AuthGuard, RolesGuard) 29 | @UseInterceptors(AuthUserInterceptor) 30 | @ApiBearerAuth() 31 | export class UserController { 32 | constructor(private _userService: UserService) {} 33 | 34 | @Get('admin') 35 | @Roles(RoleType.USER) 36 | @HttpCode(HttpStatus.OK) 37 | async admin(@AuthUser() user: UserEntity) { 38 | return 'only for you admin: ' + user.firstName; 39 | } 40 | 41 | @Get('users') 42 | @Roles(RoleType.ADMIN) 43 | @HttpCode(HttpStatus.OK) 44 | @ApiResponse({ 45 | status: HttpStatus.OK, 46 | description: 'Get users list', 47 | type: UsersPageDto, 48 | }) 49 | getUsers( 50 | @Query(new ValidationPipe({ transform: true })) 51 | pageOptionsDto: UsersPageOptionsDto, 52 | ): Promise { 53 | return this._userService.getUsers(pageOptionsDto); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/modules/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, ManyToMany, OneToMany} from 'typeorm'; 2 | 3 | import {AbstractEntity} from '../../common/abstract.entity'; 4 | import {RoleType} from '../../common/constants/role-type'; 5 | import {UserDto} from './dto/UserDto'; 6 | import {PasswordTransformer} from './password.transformer'; 7 | import {MessageEntity} from "../chat/message.entity"; 8 | import {RoomEntity} from "../chat/room.entity"; 9 | 10 | @Entity({name: 'users'}) 11 | export class UserEntity extends AbstractEntity { 12 | @Column({nullable: true}) 13 | firstName: string; 14 | 15 | @Column({nullable: true}) 16 | lastName: string; 17 | 18 | @Column({type: 'enum', enum: RoleType, default: RoleType.USER}) 19 | role: RoleType; 20 | 21 | @Column({unique: true, nullable: true}) 22 | email: string; 23 | 24 | @Column({nullable: true, transformer: new PasswordTransformer()}) 25 | password: string; 26 | 27 | @Column({nullable: true}) 28 | phone: string; 29 | 30 | 31 | //rooms that the user is joined 32 | @ManyToMany(type => RoomEntity, room => room.members) 33 | rooms: RoomEntity[]; 34 | 35 | 36 | // @OneToMany(type => MessageEntity, message => message.receiver) 37 | // received_messages: MessageEntity[]; 38 | 39 | dtoClass = UserDto; 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import {Module, forwardRef} from '@nestjs/common'; 2 | import {TypeOrmModule} from '@nestjs/typeorm'; 3 | 4 | import {UserService} from './user.service'; 5 | import {UserController} from './user.controller'; 6 | import {AuthModule} from '../auth/auth.module'; 7 | import {UserRepository} from './user.repository'; 8 | import {ChatModule} from "../chat/chat.module"; 9 | 10 | @Module({ 11 | imports: [ 12 | forwardRef(() => AuthModule), 13 | forwardRef(() => ChatModule), 14 | 15 | TypeOrmModule.forFeature([UserRepository]), 16 | ], 17 | controllers: [UserController], 18 | exports: [UserService, TypeOrmModule], 19 | providers: [UserService,], 20 | }) 21 | export class UserModule { 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/user/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from 'typeorm'; 2 | import { EntityRepository } from 'typeorm/decorator/EntityRepository'; 3 | import { UserEntity } from './user.entity'; 4 | 5 | @EntityRepository(UserEntity) 6 | export class UserRepository extends Repository {} 7 | -------------------------------------------------------------------------------- /src/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { FindConditions } from 'typeorm'; 3 | import { UserEntity } from './user.entity'; 4 | import { UserRegisterDto } from '../auth/dto/UserRegisterDto'; 5 | import { UserRepository } from './user.repository'; 6 | import { ValidatorService } from '../../shared/services/validator.service'; 7 | import { AwsS3Service } from '../../shared/services/aws-s3.service'; 8 | import { UsersPageOptionsDto } from './dto/UsersPageOptionsDto'; 9 | import { PageMetaDto } from '../../common/dto/PageMetaDto'; 10 | import { UsersPageDto } from './dto/UsersPageDto'; 11 | import {InjectRepository} from "@nestjs/typeorm"; 12 | 13 | @Injectable() 14 | export class UserService { 15 | constructor( 16 | @InjectRepository(UserRepository) 17 | public readonly userRepository: UserRepository, 18 | ) {} 19 | 20 | /** 21 | * Find single user 22 | */ 23 | findOne(findData: FindConditions): Promise { 24 | return this.userRepository.findOne(findData); 25 | } 26 | async findByUsernameOrEmail( 27 | options: Partial<{ username: string; email: string }>, 28 | ): Promise { 29 | const queryBuilder = this.userRepository.createQueryBuilder('user'); 30 | 31 | if (options.email) { 32 | queryBuilder.orWhere('user.email = :email', { 33 | email: options.email, 34 | }); 35 | } 36 | if (options.username) { 37 | queryBuilder.orWhere('user.username = :username', { 38 | username: options.username, 39 | }); 40 | } 41 | 42 | return queryBuilder.getOne(); 43 | } 44 | 45 | async createUser( 46 | userRegisterDto: UserRegisterDto, 47 | ): Promise { 48 | const user = this.userRepository.create({ ...userRegisterDto }); 49 | return this.userRepository.save(user); 50 | } 51 | 52 | async getUsers(pageOptionsDto: UsersPageOptionsDto): Promise { 53 | const queryBuilder = this.userRepository.createQueryBuilder('user'); 54 | const [users, usersCount] = await queryBuilder 55 | .skip(pageOptionsDto.skip) 56 | .take(pageOptionsDto.take) 57 | .getManyAndCount(); 58 | 59 | const pageMetaDto = new PageMetaDto({ 60 | pageOptionsDto, 61 | itemCount: usersCount, 62 | }); 63 | return new UsersPageDto(users.toDtos(), pageMetaDto); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/providers/context.service.ts: -------------------------------------------------------------------------------- 1 | import * as requestContext from 'request-context'; 2 | 3 | export class ContextService { 4 | private static readonly _nameSpace = 'request'; 5 | 6 | static get(key: string): T { 7 | return requestContext.get(ContextService._getKeyWithNamespace(key)); 8 | } 9 | 10 | static set(key: string, value: any): void { 11 | requestContext.set(ContextService._getKeyWithNamespace(key), value); 12 | } 13 | 14 | private static _getKeyWithNamespace(key: string): string { 15 | return `${ContextService._nameSpace}.${key}`; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/providers/utils.service.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | import * as _ from 'lodash'; 3 | import {RedisService} from "nestjs-redis"; 4 | 5 | export class UtilsService { 6 | 7 | /** 8 | * convert entity to dto class instance 9 | * @param {{new(entity: E, options: any): T}} model 10 | * @param {E[] | E} entity 11 | * @param options 12 | * @returns {T[] | T} 13 | */ 14 | public static toDto( 15 | model: new (entity: E, options?: any) => T, 16 | entity: E, 17 | options?: any, 18 | ): T; 19 | public static toDto( 20 | model: new (entity: E, options?: any) => T, 21 | entity: E[], 22 | options?: any, 23 | ): T[]; 24 | public static toDto( 25 | model: new (entity: E, options?: any) => T, 26 | entity: E | E[], 27 | options?: any, 28 | ): T | T[] { 29 | if (_.isArray(entity)) { 30 | return entity.map(u => new model(u, options)); 31 | } 32 | 33 | return new model(entity, options); 34 | } 35 | 36 | /** 37 | * generate hash from password or string 38 | * @param {string} password 39 | * @returns {string} 40 | */ 41 | static generateHash(password: string): string { 42 | return bcrypt.hashSync(password, 10); 43 | } 44 | 45 | /** 46 | * generate random string 47 | * @param length 48 | */ 49 | static generateRandomString(length: number) { 50 | return Math.random() 51 | .toString(36) 52 | .replace(/[^a-zA-Z0-9]+/g, '') 53 | .substr(0, length); 54 | } 55 | 56 | /** 57 | * validate text with hash 58 | * @param {string} password 59 | * @param {string} hash 60 | * @returns {Promise} 61 | */ 62 | static validateHash(password: string, hash: string): Promise { 63 | return bcrypt.compare(password, hash || ''); 64 | } 65 | 66 | 67 | /** 68 | * set user id on redis => key: user.id, value: socketId 69 | * @param redisService 70 | * @param userId 71 | * @param socketId 72 | */ 73 | static async setUserIdAndSocketIdOnRedis(redisService: RedisService, userId: string, socketId: string) { 74 | await redisService.getClient().set(`users:${userId}`, socketId, 'NX', 'EX', 30); 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/shared/services/aws-s3.service.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from 'aws-sdk'; 2 | import * as mime from 'mime-types'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ConfigService } from './config.service'; 5 | import { GeneratorService } from './generator.service'; 6 | import { IFile } from '../../interfaces/IFile'; 7 | 8 | @Injectable() 9 | export class AwsS3Service { 10 | private readonly _s3: AWS.S3; 11 | 12 | constructor( 13 | public configService: ConfigService, 14 | public generatorService: GeneratorService, 15 | ) { 16 | const options: AWS.S3.Types.ClientConfiguration = { 17 | apiVersion: '2010-12-01', 18 | region: 'eu-central-1', 19 | }; 20 | 21 | const awsS3Config = configService.awsS3Config; 22 | if (awsS3Config.accessKeyId && awsS3Config.secretAccessKey) { 23 | options.credentials = awsS3Config; 24 | } 25 | 26 | this._s3 = new AWS.S3(options); 27 | } 28 | 29 | async uploadImage(file: IFile) { 30 | const fileName = this.generatorService.fileName( 31 | mime.extension(file.mimetype), 32 | ); 33 | const key = 'images/' + fileName; 34 | await this._s3 35 | .putObject({ 36 | Bucket: this.configService.awsS3Config.bucketName, 37 | Body: file.buffer, 38 | ACL: 'public-read', 39 | Key: key, 40 | }) 41 | .promise(); 42 | 43 | return key; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/shared/services/config.service.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 3 | 4 | import { IAwsConfig } from '../../interfaces/IAwsConfig'; 5 | import { SnakeNamingStrategy } from '../../snake-naming.strategy'; 6 | 7 | export class ConfigService { 8 | constructor() { 9 | const nodeEnv = this.nodeEnv; 10 | dotenv.config({ 11 | path: `.${nodeEnv}.env`, 12 | }); 13 | 14 | // Replace \\n with \n to support multiline strings in AWS 15 | for (const envName of Object.keys(process.env)) { 16 | process.env[envName] = process.env[envName].replace(/\\n/g, '\n'); 17 | } 18 | 19 | console.info(process.env); 20 | } 21 | 22 | public get(key: string): string { 23 | return process.env[key]; 24 | } 25 | 26 | public getNumber(key: string): number { 27 | return Number(this.get(key)); 28 | } 29 | 30 | get nodeEnv(): string { 31 | return this.get('NODE_ENV') || 'development'; 32 | } 33 | 34 | get typeOrmConfig(): TypeOrmModuleOptions { 35 | let entities = [__dirname + '/../../modules/**/*.entity{.ts,.js}']; 36 | let migrations = [__dirname + '/../../migrations/*{.ts,.js}']; 37 | 38 | if ((module).hot) { 39 | const entityContext = (require).context( 40 | './../../modules', 41 | true, 42 | /\.entity\.ts$/, 43 | ); 44 | entities = entityContext.keys().map(id => { 45 | const entityModule = entityContext(id); 46 | const [entity] = Object.values(entityModule); 47 | return entity; 48 | }); 49 | const migrationContext = (require).context( 50 | './../../migrations', 51 | false, 52 | /\.ts$/, 53 | ); 54 | migrations = migrationContext.keys().map(id => { 55 | const migrationModule = migrationContext(id); 56 | const [migration] = Object.values(migrationModule); 57 | return migration; 58 | }); 59 | } 60 | return { 61 | entities, 62 | migrations, 63 | keepConnectionAlive: true, 64 | type: 'postgres', 65 | host: this.get('DATABASE_HOST'), 66 | port: this.getNumber('DATABASE_PORT'), 67 | username: this.get('DATABASE_USERNAME'), 68 | password: this.get('DATABASE_PASSWORD'), 69 | database: this.get('DATABASE_DATABASE'), 70 | migrationsRun: true, 71 | // logging: this.nodeEnv === 'development', 72 | namingStrategy: new SnakeNamingStrategy(), 73 | }; 74 | } 75 | 76 | get awsS3Config(): IAwsConfig { 77 | return { 78 | accessKeyId: this.get('AWS_S3_ACCESS_KEY_ID'), 79 | secretAccessKey: this.get('AWS_S3_SECRET_ACCESS_KEY'), 80 | bucketName: this.get('S3_BUCKET_NAME'), 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/shared/services/generator.service.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from 'uuid/v1'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class GeneratorService { 6 | public uuid(): string { 7 | return uuid(); 8 | } 9 | public fileName(ext: string) { 10 | return this.uuid() + '.' + ext; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/services/validator.service.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class ValidatorService { 6 | public isImage(mimeType: string): boolean { 7 | const imageMimeTypes = ['image/jpeg', 'image/png']; 8 | 9 | return _.includes(imageMimeTypes, mimeType); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global, HttpModule } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | 4 | import { AwsS3Service } from './services/aws-s3.service'; 5 | import { ConfigService } from './services/config.service'; 6 | import { GeneratorService } from './services/generator.service'; 7 | import { ValidatorService } from './services/validator.service'; 8 | 9 | const providers = [ 10 | ConfigService, 11 | ValidatorService, 12 | AwsS3Service, 13 | GeneratorService, 14 | ]; 15 | 16 | @Global() 17 | @Module({ 18 | providers, 19 | imports: [ 20 | HttpModule, 21 | JwtModule.registerAsync({ 22 | imports: [SharedModule], 23 | useFactory: (configService: ConfigService) => ({ 24 | secretOrPrivateKey: configService.get('JWT_SECRET_KEY'), 25 | // if you want to use token with expiration date 26 | // signOptions: { 27 | // expiresIn: configService.getNumber('JWT_EXPIRATION_TIME'), 28 | // }, 29 | }), 30 | inject: [ConfigService], 31 | }), 32 | ], 33 | exports: [...providers, HttpModule, JwtModule], 34 | }) 35 | export class SharedModule {} 36 | -------------------------------------------------------------------------------- /src/snake-naming.strategy.ts: -------------------------------------------------------------------------------- 1 | import { NamingStrategyInterface, DefaultNamingStrategy } from 'typeorm'; 2 | import { snakeCase } from 'typeorm/util/StringUtils'; 3 | 4 | export class SnakeNamingStrategy extends DefaultNamingStrategy 5 | implements NamingStrategyInterface { 6 | tableName(className: string, customName: string): string { 7 | return customName ? customName : snakeCase(className); 8 | } 9 | 10 | columnName( 11 | propertyName: string, 12 | customName: string, 13 | embeddedPrefixes: string[], 14 | ): string { 15 | return ( 16 | snakeCase(embeddedPrefixes.join('_')) + 17 | (customName ? customName : snakeCase(propertyName)) 18 | ); 19 | } 20 | 21 | relationName(propertyName: string): string { 22 | return snakeCase(propertyName); 23 | } 24 | 25 | joinColumnName(relationName: string, referencedColumnName: string): string { 26 | return snakeCase(relationName + '_' + referencedColumnName); 27 | } 28 | 29 | joinTableName( 30 | firstTableName: string, 31 | secondTableName: string, 32 | firstPropertyName: string, 33 | _secondPropertyName: string, 34 | ): string { 35 | return snakeCase( 36 | firstTableName + 37 | '_' + 38 | firstPropertyName.replace(/\./gi, '_') + 39 | '_' + 40 | secondTableName, 41 | ); 42 | } 43 | 44 | joinTableColumnName( 45 | tableName: string, 46 | propertyName: string, 47 | columnName?: string, 48 | ): string { 49 | return snakeCase( 50 | tableName + '_' + (columnName ? columnName : propertyName), 51 | ); 52 | } 53 | 54 | classTableInheritanceParentColumnName( 55 | parentTableName: any, 56 | parentTableIdPropertyName: any, 57 | ): string { 58 | return snakeCase(parentTableName + '_' + parentTableIdPropertyName); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/viveo-swagger.ts: -------------------------------------------------------------------------------- 1 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 2 | import { INestApplication } from '@nestjs/common'; 3 | 4 | export function setupSwagger(app: INestApplication) { 5 | const options = new DocumentBuilder() 6 | .setTitle('API') 7 | .setVersion('0.0.1') 8 | .addBearerAuth() 9 | .build(); 10 | 11 | const document = SwaggerModule.createDocument(app, options); 12 | SwaggerModule.setup('docs', app, document); 13 | } 14 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | Nestjs SocketIO 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |
    20 |
  • {{ room.name }}
  • 21 |
22 |
23 |
24 |
25 |

{{ title }}

26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
    34 |
  • {{ message.name }}: {{ message.text }}
  • 35 |
36 |
37 |
38 |
39 | 40 |
41 | 42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /static/main.js: -------------------------------------------------------------------------------- 1 | const app = new Vue({ 2 | el: '#app', 3 | data: { 4 | title: 'Nestjs Websockets Chat', 5 | name: '', 6 | text: '', 7 | messages: [], 8 | rooms: [], 9 | socket: null 10 | }, 11 | methods: { 12 | createPublcRoom() { 13 | const message = { 14 | name: this.name, 15 | }; 16 | this.socket.emit('createNewPublicRoom', message); 17 | this.text = '' 18 | }, 19 | joinPublicRoom() { 20 | const message = { 21 | name: 'room1', 22 | }; 23 | this.socket.emit('joinPublicRoom', message); 24 | this.text = '' 25 | }, 26 | sendMessage() { 27 | if (this.validateInput()) { 28 | const message = { 29 | room_name: 'room1', 30 | text: this.text 31 | }; 32 | this.socket.emit('msgToRoomServer', message); 33 | this.text = '' 34 | } 35 | }, 36 | sendMessagePM() { 37 | if (this.validateInput()) { 38 | const message = { 39 | text: this.text, 40 | receiver: "38856fc3-586b-4aca-97dd-9d13fc0c7580" 41 | }; 42 | this.socket.emit('msgPrivateToServer', message); 43 | this.text = '' 44 | } 45 | }, 46 | getRooms() { 47 | console.log('hii'); 48 | 49 | this.socket.emit('getRooms', {}); 50 | } 51 | , 52 | receivedMessage(message) { 53 | this.messages.push(message) 54 | }, 55 | validateInput() { 56 | return this.name.length > 0 && this.text.length > 0 57 | } 58 | }, 59 | created() { 60 | this.socket = io('http://localhost:3000', { 61 | // 'query': 'token=' + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjM4ODU2ZmMzLTU4NmItNGFjYS05N2RkLTlkMTNmYzBjNzU4MCIsImlhdCI6MTU4MTYxODI3Nn0.zvHEzdSYrs_VpCCiFA37fBOkafpC1lI-axOhkcfpmxw' 62 | 'query': 'token=' + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjE2ZDUxMzM5LWJjZTctNDQ1Mi1hY2E0LTdmMzUyMjEyNWM5MSIsImlhdCI6MTU4MTYxODMxMH0.K4D4FSODoRrmT6NQnh8d9XzhBhtDudestY98SRe62lA' 63 | 64 | }); 65 | 66 | this.socket.on('connect', (message) => { 67 | // this.receivedMessage(message) 68 | console.log(message); 69 | console.log('connected'); 70 | this.getRooms(); 71 | 72 | }); 73 | this.socket.on('msgToClient', (message) => { 74 | this.receivedMessage(message) 75 | }); 76 | 77 | this.socket.on('getRooms', (message) => { 78 | console.log(message); 79 | this.rooms = [...message]; 80 | }); 81 | 82 | this.socket.on('msgPrivateToClient', (message) => { 83 | this.receivedMessage(message) 84 | }); 85 | 86 | this.socket.on('createdNewPublicRoom', (message) => { 87 | console.log(message); 88 | }); 89 | this.socket.on('msgToRoomClient', (message) => { 90 | console.log(message); 91 | this.receivedMessage(message); 92 | }); 93 | this.socket.on('userJoined', (message) => { 94 | console.log(message); 95 | this.receivedMessage(message); 96 | }); 97 | } 98 | }); 99 | -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | #messages{ 2 | height:300px; 3 | overflow-y: scroll; 4 | } 5 | #app { 6 | margin-top: 2rem; 7 | } 8 | 9 | #app { 10 | background: #eeeeee; 11 | padding: 20px; 12 | border-radius: 10px; 13 | box-shadow: 2px 1px 1px 1px #eee; 14 | } 15 | 16 | #rooms { 17 | 18 | } 19 | #room { 20 | background: #4582ee; 21 | margin: 5px; 22 | border-radius: 4px; 23 | box-shadow: 1px 1px 1px; 24 | padding: 4px; 25 | color: #eeeeee; 26 | } 27 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../src/app.module'; 4 | 5 | describe('AuthController (e2e)', () => { 6 | let app; 7 | 8 | beforeEach(async () => { 9 | const moduleFixture: TestingModule = await Test.createTestingModule({ 10 | imports: [AppModule], 11 | }).compile(); 12 | 13 | app = moduleFixture.createNestApplication(); 14 | await app.init(); 15 | }); 16 | 17 | it('/ (GET)', () => { 18 | return request(app.getHttpServer()) 19 | .get('/') 20 | .expect(200) 21 | .expect('Hello World!'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": false, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "outDir": "./dist", 15 | "baseUrl": "./src" 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "**/*.spec.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const nodeExternals = require('webpack-node-externals'); 5 | 6 | module.exports = { 7 | entry: ['webpack/hot/poll?100', './src/main.ts'], 8 | watch: true, 9 | target: 'node', 10 | externals: [ 11 | nodeExternals({ 12 | whitelist: ['webpack/hot/poll?100'], 13 | }), 14 | ], 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.tsx?$/, 19 | use: 'ts-loader', 20 | exclude: /node_modules/, 21 | }, 22 | ], 23 | }, 24 | mode: 'development', 25 | resolve: { 26 | extensions: ['.tsx', '.ts', '.js'], 27 | }, 28 | plugins: [ 29 | new webpack.ProgressPlugin(), 30 | new CleanWebpackPlugin(), 31 | new webpack.HotModuleReplacementPlugin(), 32 | new webpack.WatchIgnorePlugin([/\.js$/, /\.d\.ts$/])], 33 | output: { 34 | path: path.join(__dirname, 'dist'), 35 | filename: 'main.js', 36 | }, 37 | }; 38 | --------------------------------------------------------------------------------