├── .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 |
--------------------------------------------------------------------------------