├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── docker-compose.yml
├── documentation
├── classes
│ ├── AddCreditCardDto.html
│ ├── Address.html
│ ├── Category.html
│ ├── CategoryNotFoundException.html
│ ├── ChatGateway.html
│ ├── CheckVerificationCodeDto.html
│ ├── Comment.html
│ ├── ConfirmEmailDto.html
│ ├── CreateCategoryDto.html
│ ├── CreateChargeDto.html
│ ├── CreateCommentCommand.html
│ ├── CreateCommentDto.html
│ ├── CreateCommentHandler.html
│ ├── CreateLogDto.html
│ ├── CreatePostDto.html
│ ├── CreatePostInput.html
│ ├── CreateProductCategoryDto.html
│ ├── CreateProductDto.html
│ ├── CreateSubscriberDto.html
│ ├── CreateUserDto.html
│ ├── DatabaseLogger.html
│ ├── EmailScheduleDto.html
│ ├── FindOneParams.html
│ ├── GetCommentsDto.html
│ ├── GetCommentsHandler.html
│ ├── GetCommentsQuery.html
│ ├── Log.html
│ ├── LogInDto.html
│ ├── Message.html
│ ├── ObjectWithIdDto.html
│ ├── PaginationParams.html
│ ├── Post-1.html
│ ├── Post.html
│ ├── PostNotFoundException.html
│ ├── PostsResolver.html
│ ├── Product.html
│ ├── ProductCategory.html
│ ├── PublicFile.html
│ ├── RegisterDto.html
│ ├── SetDefaultCreditCardDto.html
│ ├── StripeEvent.html
│ ├── Timestamp.html
│ ├── TokenVerificationDto.html
│ ├── TwoFactorAuthenticationCodeDto.html
│ ├── UpdateCategoryDto.html
│ ├── UpdatePostDto.html
│ ├── User-1.html
│ └── User.html
├── controllers
│ ├── AuthenticationController.html
│ ├── CategoriesController.html
│ ├── ChargeController.html
│ ├── CommentsController.html
│ ├── CreditCardsController.html
│ ├── EmailConfirmationController.html
│ ├── EmailSchedulingController.html
│ ├── GoogleAuthenticationController.html
│ ├── HealthController.html
│ ├── OptimizeController.html
│ ├── PostsController.html
│ ├── ProductCategoriesController.html
│ ├── ProductsController.html
│ ├── SmsController.html
│ ├── StripeWebhookController.html
│ ├── SubscribersController.html
│ ├── SubscriptionsController.html
│ ├── TwoFactorAuthenticationController.html
│ └── UsersController.html
├── coverage.html
├── dependencies.html
├── fonts
│ ├── ionicons.eot
│ ├── ionicons.svg
│ ├── ionicons.ttf
│ ├── ionicons.woff
│ ├── ionicons.woff2
│ ├── roboto-v15-latin-300.eot
│ ├── roboto-v15-latin-300.svg
│ ├── roboto-v15-latin-300.ttf
│ ├── roboto-v15-latin-300.woff
│ ├── roboto-v15-latin-300.woff2
│ ├── roboto-v15-latin-700.eot
│ ├── roboto-v15-latin-700.svg
│ ├── roboto-v15-latin-700.ttf
│ ├── roboto-v15-latin-700.woff
│ ├── roboto-v15-latin-700.woff2
│ ├── roboto-v15-latin-italic.eot
│ ├── roboto-v15-latin-italic.svg
│ ├── roboto-v15-latin-italic.ttf
│ ├── roboto-v15-latin-italic.woff
│ ├── roboto-v15-latin-italic.woff2
│ ├── roboto-v15-latin-regular.eot
│ ├── roboto-v15-latin-regular.svg
│ ├── roboto-v15-latin-regular.ttf
│ ├── roboto-v15-latin-regular.woff
│ └── roboto-v15-latin-regular.woff2
├── graph
│ └── dependencies.svg
├── guards
│ └── EmailConfirmationGuard.html
├── images
│ ├── compodoc-vectorise-inverted.png
│ ├── compodoc-vectorise-inverted.svg
│ ├── compodoc-vectorise.png
│ ├── compodoc-vectorise.svg
│ ├── coverage-badge-documentation.svg
│ └── favicon.ico
├── index.html
├── injectables
│ ├── AuthenticationService.html
│ ├── CategoriesService.html
│ ├── ChatService.html
│ ├── CustomLogger.html
│ ├── ElasticsearchHealthIndicator.html
│ ├── EmailConfirmationService.html
│ ├── EmailSchedulingService.html
│ ├── EmailService.html
│ ├── ExcludeNullInterceptor.html
│ ├── FilesService.html
│ ├── GoogleAuthenticationService.html
│ ├── GraphqlJwtAuthGuard.html
│ ├── HttpCacheInterceptor.html
│ ├── JwtAuthenticationGuard.html
│ ├── JwtRefreshGuard.html
│ ├── JwtRefreshTokenStrategy.html
│ ├── JwtStrategy.html
│ ├── JwtTwoFactorGuard.html
│ ├── JwtTwoFactorStrategy.html
│ ├── LocalAuthenticationGuard.html
│ ├── LocalStrategy.html
│ ├── LogsMiddleware.html
│ ├── LogsService.html
│ ├── PostsLoaders.html
│ ├── PostsSearchService.html
│ ├── PostsService.html
│ ├── ProductCategoriesService.html
│ ├── ProductsService.html
│ ├── SmsService.html
│ ├── StripeService.html
│ ├── StripeWebhookService.html
│ ├── SubscriptionsService.html
│ ├── TwoFactorAuthenticationService.html
│ └── UsersService.html
├── interfaces
│ ├── BookProperties.html
│ ├── CarProperties.html
│ ├── PostCountResult.html
│ ├── PostSearchBody.html
│ ├── PostSearchResult.html
│ ├── RequestWithRawBody.html
│ ├── RequestWithUser.html
│ ├── Subscriber.html
│ ├── SubscribersService.html
│ ├── TokenPayload.html
│ └── VerificationTokenPayload.html
├── js
│ ├── compodoc.js
│ ├── lazy-load-graphs.js
│ ├── libs
│ │ ├── EventDispatcher.js
│ │ ├── bootstrap-native.js
│ │ ├── clipboard.min.js
│ │ ├── custom-elements-es5-adapter.js
│ │ ├── custom-elements.min.js
│ │ ├── d3.v3.min.js
│ │ ├── deep-iterator.js
│ │ ├── es6-shim.min.js
│ │ ├── htmlparser.js
│ │ ├── innersvg.js
│ │ ├── lit-html.js
│ │ ├── prism.js
│ │ ├── promise.min.js
│ │ ├── svg-pan-zoom.min.js
│ │ ├── tablesort.min.js
│ │ ├── tablesort.number.min.js
│ │ ├── vis.min.js
│ │ └── zepto.min.js
│ ├── menu-wc.js
│ ├── menu-wc_es5.js
│ ├── menu.js
│ ├── routes.js
│ ├── search
│ │ ├── lunr.min.js
│ │ ├── search-lunr.js
│ │ ├── search.js
│ │ └── search_index.js
│ ├── sourceCode.js
│ ├── svg-pan-zoom.controls.js
│ ├── tabs.js
│ └── tree.js
├── miscellaneous
│ ├── enumerations.html
│ ├── functions.html
│ └── variables.html
├── modules.html
├── modules
│ ├── AppModule.html
│ ├── AppModule
│ │ └── dependencies.svg
│ ├── AuthenticationModule.html
│ ├── AuthenticationModule
│ │ └── dependencies.svg
│ ├── CategoriesModule.html
│ ├── CategoriesModule
│ │ └── dependencies.svg
│ ├── ChargeModule.html
│ ├── ChargeModule
│ │ └── dependencies.svg
│ ├── ChatModule.html
│ ├── ChatModule
│ │ └── dependencies.svg
│ ├── CommentsModule.html
│ ├── CreditCardsModule.html
│ ├── CreditCardsModule
│ │ └── dependencies.svg
│ ├── DatabaseModule.html
│ ├── EmailConfirmationModule.html
│ ├── EmailConfirmationModule
│ │ └── dependencies.svg
│ ├── EmailModule.html
│ ├── EmailModule
│ │ └── dependencies.svg
│ ├── EmailSchedulingModule.html
│ ├── EmailSchedulingModule
│ │ └── dependencies.svg
│ ├── FilesModule.html
│ ├── FilesModule
│ │ └── dependencies.svg
│ ├── GoogleAuthenticationModule.html
│ ├── GoogleAuthenticationModule
│ │ └── dependencies.svg
│ ├── HealthModule.html
│ ├── HealthModule
│ │ └── dependencies.svg
│ ├── LoggerModule.html
│ ├── LoggerModule
│ │ └── dependencies.svg
│ ├── OptimizeModule.html
│ ├── PostsModule.html
│ ├── PostsModule
│ │ └── dependencies.svg
│ ├── ProductCategoriesModule.html
│ ├── ProductCategoriesModule
│ │ └── dependencies.svg
│ ├── ProductsModule.html
│ ├── ProductsModule
│ │ └── dependencies.svg
│ ├── PubSubModule.html
│ ├── SearchModule.html
│ ├── SmsModule.html
│ ├── SmsModule
│ │ └── dependencies.svg
│ ├── StripeModule.html
│ ├── StripeModule
│ │ └── dependencies.svg
│ ├── StripeWebhookModule.html
│ ├── StripeWebhookModule
│ │ └── dependencies.svg
│ ├── SubscribersModule.html
│ ├── SubscriptionsModule.html
│ ├── SubscriptionsModule
│ │ └── dependencies.svg
│ ├── UsersModule.html
│ └── UsersModule
│ │ └── dependencies.svg
├── overview.html
└── styles
│ ├── bootstrap-card.css
│ ├── bootstrap.min.css
│ ├── compodoc.css
│ ├── dark.css
│ ├── ionicons.min.css
│ ├── laravel.css
│ ├── material.css
│ ├── original.css
│ ├── postmark.css
│ ├── prism.css
│ ├── readthedocs.css
│ ├── reset.css
│ ├── stripe.css
│ ├── style.css
│ ├── tablesort.css
│ └── vagrant.css
├── nest-cli.json
├── package-lock.json
├── package.json
├── src
├── app.module.ts
├── authentication
│ ├── authentication.controller.ts
│ ├── authentication.module.ts
│ ├── authentication.service.ts
│ ├── dto
│ │ ├── logIn.dto.ts
│ │ └── register.dto.ts
│ ├── graphql-jwt-auth.guard.ts
│ ├── jwt-authentication.guard.ts
│ ├── jwt-refresh-token.strategy.ts
│ ├── jwt-refresh.guard.ts
│ ├── jwt-two-factor.guard.ts
│ ├── jwt-two-factor.strategy.ts
│ ├── jwt.strategy.ts
│ ├── local.strategy.ts
│ ├── localAuthentication.guard.ts
│ ├── requestWithUser.interface.ts
│ ├── tests
│ │ ├── authentication.controller.integration-spec.ts
│ │ ├── authentication.service.integration-spec.ts
│ │ ├── authentication.service.spec.ts
│ │ └── user.mock.ts
│ ├── tokenPayload.interface.ts
│ └── twoFactor
│ │ ├── dto
│ │ └── twoFactorAuthenticationCode.dto.ts
│ │ ├── twoFactorAuthentication.controller.ts
│ │ └── twoFactorAuthentication.service.ts
├── categories
│ ├── categories.controller.ts
│ ├── categories.module.ts
│ ├── categories.service.md
│ ├── categories.service.ts
│ ├── category.entity.ts
│ ├── dto
│ │ ├── createCategory.dto.ts
│ │ └── updateCategory.dto.ts
│ └── exceptions
│ │ └── categoryNotFound.exception.ts
├── charge
│ ├── charge.controller.ts
│ ├── charge.module.ts
│ └── dto
│ │ └── createCharge.dto.ts
├── chat
│ ├── chat.gateway.ts
│ ├── chat.module.ts
│ ├── chat.service.ts
│ └── message.entity.ts
├── comments
│ ├── commands
│ │ ├── handlers
│ │ │ └── create-comment.handler.ts
│ │ └── implementations
│ │ │ └── createComment.command.ts
│ ├── comment.entity.ts
│ ├── comments.controller.ts
│ ├── comments.module.ts
│ ├── dto
│ │ ├── createComment.dto.ts
│ │ └── getComments.dto.ts
│ └── queries
│ │ ├── handlers
│ │ └── getComments.handler.ts
│ │ └── implementations
│ │ └── getComments.query.ts
├── credit-cards
│ ├── creditCards.controller.ts
│ ├── creditCards.module.ts
│ └── dto
│ │ ├── addCreditCardDto.ts
│ │ └── setDefaultCreditCard.dto.ts
├── database
│ ├── database.module.ts
│ ├── databaseLogger.ts
│ └── postgresErrorCode.enum.ts
├── databaseFiles
│ ├── databaseFile.entity.ts
│ ├── databaseFiles.module.ts
│ ├── databaseFiles.services.ts
│ └── databaseFilesController.ts
├── email
│ ├── email.module-definition.ts
│ ├── email.module.ts
│ ├── email.service.ts
│ ├── emailAsyncOptions.type.ts
│ └── emailOptions.interface.ts
├── emailConfirmation
│ ├── confirmEmail.dto.ts
│ ├── emailConfirmation.controller.ts
│ ├── emailConfirmation.guard.ts
│ ├── emailConfirmation.module.ts
│ ├── emailConfirmation.service.ts
│ └── verificationTokenPayload.interface.ts
├── emailScheduling
│ ├── dto
│ │ └── emailSchedule.dto.ts
│ ├── emailScheduling.controller.ts
│ ├── emailScheduling.module.ts
│ └── emailScheduling.service.ts
├── files
│ ├── files.module.ts
│ ├── files.service.ts
│ └── publicFile.entity.ts
├── googleAuthentication
│ ├── googleAuthentication.controller.ts
│ ├── googleAuthentication.module.ts
│ ├── googleAuthentication.service.ts
│ └── tokenVerification.dto.ts
├── health
│ ├── elasticsearchHealthIndicator.ts
│ ├── health.controller.ts
│ └── health.module.ts
├── localFiles
│ ├── localFile.dto.ts
│ ├── localFile.entity.ts
│ ├── localFiles.controller.ts
│ ├── localFiles.interceptor.ts
│ ├── localFiles.module.ts
│ └── localFiles.service.ts
├── logger
│ ├── customLogger.ts
│ ├── dto
│ │ └── createLog.dto.ts
│ ├── log.entity.ts
│ ├── logger.module.ts
│ └── logs.service.ts
├── main.ts
├── optimize
│ ├── image.processor.ts
│ ├── optimize.controller.ts
│ └── optimize.module.ts
├── posts
│ ├── dto
│ │ ├── createPost.dto.ts
│ │ └── updatePost.dto.ts
│ ├── exceptions
│ │ └── postNotFound.exception.ts
│ ├── httpCache.interceptor.ts
│ ├── inputs
│ │ └── post.input.ts
│ ├── loaders
│ │ └── posts.loaders.ts
│ ├── models
│ │ └── post.model.ts
│ ├── post.entity.ts
│ ├── posts.controller.ts
│ ├── posts.module.ts
│ ├── posts.resolver.ts
│ ├── posts.service.ts
│ ├── postsCacheKey.constant.ts
│ ├── postsSearch.service.ts
│ └── types
│ │ ├── postCountBody.interface.ts
│ │ ├── postSearchBody.interface.ts
│ │ └── postSearchResponse.interface.ts
├── productCategories
│ ├── dto
│ │ └── createProductCategory.dto.ts
│ ├── productCategories.controller.ts
│ ├── productCategories.module.ts
│ ├── productCategories.service.ts
│ └── productCategory.entity.ts
├── products
│ ├── dto
│ │ └── createProduct.dto.ts
│ ├── product.entity.ts
│ ├── products.controller.ts
│ ├── products.module.ts
│ ├── products.service.ts
│ └── types
│ │ ├── bookProperties.interface.ts
│ │ └── carProperties.interface.ts
├── pubSub
│ └── pubSub.module.ts
├── repl.ts
├── schema.gql
├── search
│ └── search.module.ts
├── sms
│ ├── checkVerificationCode.dto.ts
│ ├── sms.controller.ts
│ ├── sms.module.ts
│ └── sms.service.ts
├── stripe
│ ├── stripe.module.ts
│ └── stripe.service.ts
├── stripeWebhook
│ ├── StripeEvent.entity.ts
│ ├── requestWithRawBody.interface.ts
│ ├── stripeWebhook.controller.ts
│ ├── stripeWebhook.module.ts
│ └── stripeWebhook.service.ts
├── subscribers
│ ├── dto
│ │ └── createSubscriber.dto.ts
│ ├── subscriber.service.ts
│ ├── subscribers.controller.ts
│ ├── subscribers.module.ts
│ ├── subscribers.proto
│ └── subscribers.service.interface.ts
├── subscriptions
│ ├── subscriptions.controller.ts
│ ├── subscriptions.module.ts
│ └── subscriptions.service.ts
├── users
│ ├── address.entity.ts
│ ├── dto
│ │ ├── createUser.dto.ts
│ │ └── fileUpload.dto.ts
│ ├── models
│ │ └── user.model.ts
│ ├── tests
│ │ └── users.service.spec.ts
│ ├── user.entity.ts
│ ├── users.controller.ts
│ ├── users.module.ts
│ └── users.service.ts
└── utils
│ ├── excludeNull.interceptor.ts
│ ├── findOneParams.ts
│ ├── getLogLevels.ts
│ ├── logs.middleware.ts
│ ├── mocks
│ ├── config.service.ts
│ └── jwt.service.ts
│ ├── rawBody.middleware.ts
│ ├── recursivelyStripNullValues.ts
│ ├── runInCluster.ts
│ ├── scalars
│ └── timestamp.scalar.ts
│ ├── stripeError.enum.ts
│ └── types
│ ├── cacheManagerRedisStore.d.ts
│ ├── objectWithId.dto.ts
│ └── paginationParams.ts
├── test
├── app.e2e-spec.ts
└── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: [
9 | 'plugin:@typescript-eslint/eslint-recommended',
10 | 'plugin:@typescript-eslint/recommended',
11 | 'prettier',
12 | 'prettier/@typescript-eslint',
13 | ],
14 | root: true,
15 | env: {
16 | node: true,
17 | jest: true,
18 | },
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | '@typescript-eslint/camelcase': 'off',
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | postgres:
4 | container_name: postgres
5 | image: postgres:latest
6 | ports:
7 | - "5432:5432"
8 | volumes:
9 | - /data/postgres:/data/postgres
10 | env_file:
11 | - docker.env
12 | networks:
13 | - postgres
14 |
15 | pgadmin:
16 | links:
17 | - postgres:postgres
18 | container_name: pgadmin
19 | image: dpage/pgadmin4
20 | ports:
21 | - "8080:80"
22 | volumes:
23 | - /data/pgadmin:/root/.pgadmin
24 | env_file:
25 | - docker.env
26 | networks:
27 | - postgres
28 |
29 | redis:
30 | image: "redis:alpine"
31 | ports:
32 | - "6379:6379"
33 |
34 | redis-commander:
35 | image: rediscommander/redis-commander:latest
36 | environment:
37 | - REDIS_HOSTS=local:redis:6379
38 | ports:
39 | - "8081:8081"
40 | depends_on:
41 | - redis
42 |
43 | es01:
44 | image: docker.elastic.co/elasticsearch/elasticsearch:7.9.1
45 | container_name: es01
46 | environment:
47 | - node.name=es01
48 | - cluster.name=es-docker-cluster
49 | - discovery.seed_hosts=es02,es03
50 | - cluster.initial_master_nodes=es01,es02,es03
51 | - bootstrap.memory_lock=true
52 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
53 | ulimits:
54 | memlock:
55 | soft: -1
56 | hard: -1
57 | volumes:
58 | - data01:/usr/share/elasticsearch/data
59 | ports:
60 | - 9200:9200
61 | networks:
62 | - elastic
63 | es02:
64 | image: docker.elastic.co/elasticsearch/elasticsearch:7.9.1
65 | container_name: es02
66 | environment:
67 | - node.name=es02
68 | - cluster.name=es-docker-cluster
69 | - discovery.seed_hosts=es01,es03
70 | - cluster.initial_master_nodes=es01,es02,es03
71 | - bootstrap.memory_lock=true
72 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
73 | ulimits:
74 | memlock:
75 | soft: -1
76 | hard: -1
77 | volumes:
78 | - data02:/usr/share/elasticsearch/data
79 | networks:
80 | - elastic
81 | es03:
82 | image: docker.elastic.co/elasticsearch/elasticsearch:7.9.1
83 | container_name: es03
84 | environment:
85 | - node.name=es03
86 | - cluster.name=es-docker-cluster
87 | - discovery.seed_hosts=es01,es02
88 | - cluster.initial_master_nodes=es01,es02,es03
89 | - bootstrap.memory_lock=true
90 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
91 | ulimits:
92 | memlock:
93 | soft: -1
94 | hard: -1
95 | volumes:
96 | - data03:/usr/share/elasticsearch/data
97 | networks:
98 | - elastic
99 |
100 | volumes:
101 | data01:
102 | driver: local
103 | data02:
104 | driver: local
105 | data03:
106 | driver: local
107 |
108 | networks:
109 | postgres:
110 | driver: bridge
111 | elastic:
112 | driver: bridge
--------------------------------------------------------------------------------
/documentation/fonts/ionicons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/ionicons.eot
--------------------------------------------------------------------------------
/documentation/fonts/ionicons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/ionicons.ttf
--------------------------------------------------------------------------------
/documentation/fonts/ionicons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/ionicons.woff
--------------------------------------------------------------------------------
/documentation/fonts/ionicons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/ionicons.woff2
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-300.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-300.eot
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-300.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-300.ttf
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-300.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-300.woff
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-300.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-300.woff2
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-700.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-700.eot
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-700.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-700.ttf
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-700.woff
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-700.woff2
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-italic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-italic.eot
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-italic.ttf
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-italic.woff
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-italic.woff2
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-regular.eot
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-regular.ttf
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-regular.woff
--------------------------------------------------------------------------------
/documentation/fonts/roboto-v15-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/fonts/roboto-v15-latin-regular.woff2
--------------------------------------------------------------------------------
/documentation/images/compodoc-vectorise-inverted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/images/compodoc-vectorise-inverted.png
--------------------------------------------------------------------------------
/documentation/images/compodoc-vectorise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/images/compodoc-vectorise.png
--------------------------------------------------------------------------------
/documentation/images/coverage-badge-documentation.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/documentation/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwanago/nestjs-typescript/8a7fd05b015a7596213bfbac64cf08467b92a4fd/documentation/images/favicon.ico
--------------------------------------------------------------------------------
/documentation/js/compodoc.js:
--------------------------------------------------------------------------------
1 | var compodoc = {
2 | EVENTS: {
3 | READY: 'compodoc.ready',
4 | SEARCH_READY: 'compodoc.search.ready'
5 | }
6 | };
7 |
8 | Object.assign( compodoc, EventDispatcher.prototype );
9 |
10 | document.addEventListener('DOMContentLoaded', function() {
11 | compodoc.dispatchEvent({
12 | type: compodoc.EVENTS.READY
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/documentation/js/lazy-load-graphs.js:
--------------------------------------------------------------------------------
1 | document.addEventListener('DOMContentLoaded', function() {
2 | var lazyGraphs = [].slice.call(document.querySelectorAll('[lazy]'));
3 | var active = false;
4 |
5 | var lazyLoad = function() {
6 | if (active === false) {
7 | active = true;
8 |
9 | setTimeout(function() {
10 | lazyGraphs.forEach(function(lazyGraph) {
11 | if (
12 | lazyGraph.getBoundingClientRect().top <= window.innerHeight &&
13 | lazyGraph.getBoundingClientRect().bottom >= 0 &&
14 | getComputedStyle(lazyGraph).display !== 'none'
15 | ) {
16 | lazyGraph.data = lazyGraph.getAttribute('lazy');
17 | lazyGraph.removeAttribute('lazy');
18 |
19 | lazyGraphs = lazyGraphs.filter(function(image) { return image !== lazyGraph});
20 |
21 | if (lazyGraphs.length === 0) {
22 | document.removeEventListener('scroll', lazyLoad);
23 | window.removeEventListener('resize', lazyLoad);
24 | window.removeEventListener('orientationchange', lazyLoad);
25 | }
26 | }
27 | });
28 |
29 | active = false;
30 | }, 200);
31 | }
32 | };
33 |
34 | // initial load
35 | lazyLoad();
36 |
37 | var container = document.querySelector('.container-fluid.modules');
38 | if (container) {
39 | container.addEventListener('scroll', lazyLoad);
40 | window.addEventListener('resize', lazyLoad);
41 | window.addEventListener('orientationchange', lazyLoad);
42 | }
43 |
44 | });
45 |
--------------------------------------------------------------------------------
/documentation/js/libs/EventDispatcher.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author mrdoob / http://mrdoob.com/
3 | */
4 |
5 | var EventDispatcher=function(){};Object.assign(EventDispatcher.prototype,{addEventListener:function(i,t){void 0===this._listeners&&(this._listeners={});var e=this._listeners;void 0===e[i]&&(e[i]=[]),-1===e[i].indexOf(t)&&e[i].push(t)},hasEventListener:function(i,t){if(void 0===this._listeners)return!1;var e=this._listeners;return void 0!==e[i]&&-1!==e[i].indexOf(t)},removeEventListener:function(i,t){if(void 0!==this._listeners){var e=this._listeners[i];if(void 0!==e){var s=e.indexOf(t);-1!==s&&e.splice(s,1)}}},dispatchEvent:function(i){if(void 0!==this._listeners){var t=this._listeners[i.type];if(void 0!==t){i.target=this;var e=[],s=0,n=t.length;for(s=0;s",">"));else if(1==i){if(r.push("<",e.tagName),e.hasAttributes())for(var n=e.attributes,s=0,o=n.length;s");for(var h=e.childNodes,s=0,o=h.length;s")}else r.push("/>")}else{if(8!=i)throw"Error serializing XML. Unhandled node of type: "+i;r.push("\x3c!--",e.nodeValue,"--\x3e")}};Object.defineProperty(e.prototype,"innerHTML",{get:function(){for(var e=[],r=this.firstChild;r;)t(r,e),r=r.nextSibling;return e.join("")},set:function(e){for(;this.firstChild;)this.removeChild(this.firstChild);try{var t=new DOMParser;t.async=!1,sXML="";for(var r=t.parseFromString(sXML,"text/xml").documentElement.firstChild;r;)this.appendChild(this.ownerDocument.importNode(r,!0)),r=r.nextSibling}catch(e){throw new Error("Error parsing XML string")}}})}}((0,eval)("this").SVGElement);
--------------------------------------------------------------------------------
/documentation/js/libs/promise.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2012-2013 (c) Pierre Duquesne
3 | * Licensed under the New BSD License.
4 | * https://github.com/stackp/promisejs
5 | */
6 | (function(a){function b(){this._callbacks=[];}b.prototype.then=function(a,c){var d;if(this._isdone)d=a.apply(c,this.result);else{d=new b();this._callbacks.push(function(){var b=a.apply(c,arguments);if(b&&typeof b.then==='function')b.then(d.done,d);});}return d;};b.prototype.done=function(){this.result=arguments;this._isdone=true;for(var a=0;a=300)&&j.status!==304);h.done(a,j.responseText,j);}};j.send(k);return h;}function h(a){return function(b,c,d){return g(a,b,c,d);};}var i={Promise:b,join:c,chain:d,ajax:g,get:h('GET'),post:h('POST'),put:h('PUT'),del:h('DELETE'),ENOXHR:1,ETIMEOUT:2,ajaxTimeout:0};if(typeof define==='function'&&define.amd)define(function(){return i;});else a.promise=i;})(this);
--------------------------------------------------------------------------------
/documentation/js/libs/tablesort.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * tablesort v5.1.0 (2018-09-14)
3 | * http://tristen.ca/tablesort/demo/
4 | * Copyright (c) 2018 ; Licensed MIT
5 | */
6 | !function(){function a(b,c){if(!(this instanceof a))return new a(b,c);if(!b||"TABLE"!==b.tagName)throw new Error("Element must be a table");this.init(b,c||{})}var b=[],c=function(a){var b;return window.CustomEvent&&"function"==typeof window.CustomEvent?b=new CustomEvent(a):(b=document.createEvent("CustomEvent"),b.initCustomEvent(a,!1,!1,void 0)),b},d=function(a){return a.getAttribute("data-sort")||a.textContent||a.innerText||""},e=function(a,b){return a=a.trim().toLowerCase(),b=b.trim().toLowerCase(),a===b?0:a0)if(a.tHead&&a.tHead.rows.length>0){for(e=0;e0&&l.push(k),m++;if(!l)return}for(m=0;m 0) {
9 | tabs = tabs[0].querySelectorAll('li');
10 | for (var i = 0; i < tabs.length; i++) {
11 | tabs[i].addEventListener('click', updateAddress);
12 | var linkTag = tabs[i].querySelector('a');
13 | if (location.hash !== '') {
14 | var currentHash = location.hash.substr(1);
15 | if (currentHash === linkTag.dataset.link) {
16 | linkTag.click();
17 | }
18 | }
19 | }
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/documentation/modules/CategoriesModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
41 |
--------------------------------------------------------------------------------
/documentation/modules/ChargeModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
41 |
--------------------------------------------------------------------------------
/documentation/modules/ChatModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
57 |
--------------------------------------------------------------------------------
/documentation/modules/CreditCardsModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
41 |
--------------------------------------------------------------------------------
/documentation/modules/EmailModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
57 |
--------------------------------------------------------------------------------
/documentation/modules/FilesModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
57 |
--------------------------------------------------------------------------------
/documentation/modules/ProductCategoriesModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
41 |
--------------------------------------------------------------------------------
/documentation/modules/ProductsModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
41 |
--------------------------------------------------------------------------------
/documentation/modules/StripeModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
57 |
--------------------------------------------------------------------------------
/documentation/styles/dark.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #212121;
3 | color: #fafafa;
4 | }
5 |
6 | code {
7 | color: #e09393;
8 | }
9 |
10 | a,
11 | .menu ul.list li a.active {
12 | color: #7fc9ff;
13 | }
14 |
15 | .menu {
16 | background: #212121;
17 | border-right: 1px solid #444;
18 | }
19 |
20 | .menu ul.list li a {
21 | color: #fafafa;
22 | }
23 |
24 | .menu ul.list li.divider {
25 | background: #444;
26 | }
27 |
28 | .xs-menu ul.list li:nth-child(2) {
29 | margin: 0;
30 | background: none;
31 | }
32 |
33 | .menu ul.list li:nth-child(2) {
34 | margin: 0;
35 | background: none;
36 | }
37 |
38 | #book-search-input {
39 | background: #212121;
40 | border-top: 1px solid #444;
41 | border-bottom: 1px solid #444;
42 | color: #fafafa;
43 | }
44 |
45 | .table-bordered {
46 | border: 1px solid #444;
47 | }
48 |
49 | .table-bordered > tbody > tr > td,
50 | .table-bordered > tbody > tr > th,
51 | .table-bordered > tfoot > tr > td,
52 | .table-bordered > tfoot > tr > th,
53 | .table-bordered > thead > tr > td,
54 | .table-bordered > thead > tr > th {
55 | border: 1px solid #444;
56 | }
57 |
58 | .coverage a,
59 | .coverage-count {
60 | color: #fafafa;
61 | }
62 |
63 | .coverage-header {
64 | color: black;
65 | }
66 |
67 | .routes svg text,
68 | .routes svg a {
69 | fill: white;
70 | }
71 | .routes svg rect {
72 | fill: #212121 !important;
73 | }
74 |
75 | .navbar-default,
76 | .btn-default {
77 | background-color: black;
78 | border-color: #444;
79 | color: #fafafa;
80 | }
81 |
82 | .navbar-default .navbar-brand {
83 | color: #fafafa;
84 | }
85 |
86 | .overview .card,
87 | .modules .card {
88 | background: #171717;
89 | color: #fafafa;
90 | border: 1px solid #444;
91 | }
92 | .overview .card a {
93 | color: #fafafa;
94 | }
95 |
96 | .modules .card-header {
97 | background: none;
98 | border-bottom: 1px solid #444;
99 | }
100 |
101 | .module .list-group-item {
102 | background: none;
103 | border: 1px solid #444;
104 | }
105 |
106 | .container-fluid.module h3 a {
107 | color: #337ab7;
108 | }
109 |
110 | table.params thead {
111 | background: #484848;
112 | color: #fafafa;
113 | }
114 |
--------------------------------------------------------------------------------
/documentation/styles/laravel.css:
--------------------------------------------------------------------------------
1 | .nav-tabs > li > a {
2 | text-decoration: none;
3 | }
4 |
5 | .navbar-default .navbar-brand {
6 | color: #f4645f;
7 | text-decoration: none;
8 | font-size: 16px;
9 | }
10 |
11 | .menu ul.list li a[data-type='chapter-link'],
12 | .menu ul.list li.chapter .simple {
13 | color: #525252;
14 | border-bottom: 1px dashed rgba(0, 0, 0, 0.1);
15 | }
16 |
17 | .content h1,
18 | .content h2,
19 | .content h3,
20 | .content h4,
21 | .content h5 {
22 | color: #292e31;
23 | font-weight: normal;
24 | }
25 |
26 | .content {
27 | color: #4c555a;
28 | }
29 |
30 | a {
31 | color: #f4645f;
32 | text-decoration: underline;
33 | }
34 | a:hover {
35 | color: #f1362f;
36 | }
37 |
38 | .menu ul.list li:nth-child(2) {
39 | margin-top: 0;
40 | }
41 |
42 | .menu ul.list li.title a {
43 | color: #f4645f;
44 | text-decoration: none;
45 | font-size: 16px;
46 | }
47 |
48 | .menu ul.list li a {
49 | color: #f4645f;
50 | text-decoration: none;
51 | }
52 | .menu ul.list li a.active {
53 | color: #f4645f;
54 | font-weight: bold;
55 | }
56 |
57 | code {
58 | box-sizing: border-box;
59 | display: inline-block;
60 | padding: 0 5px;
61 | background: #f0f2f1;
62 | border-radius: 3px;
63 | color: #b93d6a;
64 | font-size: 13px;
65 | line-height: 20px;
66 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.125);
67 | }
68 |
69 | pre {
70 | margin: 0;
71 | padding: 12px 12px;
72 | background: rgba(238, 238, 238, 0.35);
73 | border-radius: 3px;
74 | font-size: 13px;
75 | line-height: 1.5em;
76 | font-weight: 500;
77 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.125);
78 | }
79 |
80 | @media (prefers-color-scheme: dark) {
81 | body {
82 | color: #fafafa;
83 | }
84 | .content h1,
85 | .content h2,
86 | .content h3,
87 | .content h4,
88 | .content h5 {
89 | color: #fafafa;
90 | }
91 |
92 | code {
93 | background: none;
94 | }
95 |
96 | .content {
97 | color: #fafafa;
98 | }
99 |
100 | .menu ul.list li a[data-type='chapter-link'],
101 | .menu ul.list li.chapter .simple {
102 | color: #fafafa;
103 | }
104 |
105 | .menu ul.list li.title a {
106 | color: #fafafa;
107 | }
108 |
109 | .menu ul.list li a {
110 | color: #fafafa;
111 | }
112 | .menu ul.list li a.active {
113 | color: #7fc9ff;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/documentation/styles/material.css:
--------------------------------------------------------------------------------
1 | .menu {
2 | background: none;
3 | }
4 |
5 | a:hover {
6 | text-decoration: none;
7 | }
8 |
9 | /** LINK **/
10 |
11 | .menu ul.list li a {
12 | text-decoration: none;
13 | }
14 |
15 | .menu ul.list li a:hover,
16 | .menu ul.list li.chapter .simple:hover {
17 | background-color: #f8f9fa;
18 | text-decoration: none;
19 | }
20 |
21 | #book-search-input {
22 | margin-bottom: 0;
23 | }
24 |
25 | .menu ul.list li.divider {
26 | margin-top: 0;
27 | background: #e9ecef;
28 | }
29 |
30 | .menu .title:hover {
31 | background-color: #f8f9fa;
32 | }
33 |
34 | /** CARD **/
35 |
36 | .card {
37 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2),
38 | 0 1px 5px 0 rgba(0, 0, 0, 0.12);
39 | border-radius: 0.125rem;
40 | border: 0;
41 | margin-top: 1px;
42 | }
43 |
44 | .card-header {
45 | background: none;
46 | }
47 |
48 | /** BUTTON **/
49 |
50 | .btn {
51 | border-radius: 0.125rem;
52 | }
53 |
54 | /** NAV BAR **/
55 |
56 | .nav {
57 | border: 0;
58 | }
59 | .nav-tabs > li > a {
60 | border: 0;
61 | border-bottom: 0.214rem solid transparent;
62 | color: rgba(0, 0, 0, 0.54);
63 | margin-right: 0;
64 | }
65 | .nav-tabs > li.active > a,
66 | .nav-tabs > li.active > a:focus,
67 | .nav-tabs > li.active > a:hover {
68 | color: rgba(0, 0, 0, 0.87);
69 | border-top: 0;
70 | border-left: 0;
71 | border-right: 0;
72 | border-bottom: 0.214rem solid transparent;
73 | border-color: #008cff;
74 | font-weight: bold;
75 | }
76 | .nav > li > a:focus,
77 | .nav > li > a:hover {
78 | background: none;
79 | }
80 |
81 | /** LIST **/
82 |
83 | .list-group-item:first-child {
84 | border-top-left-radius: 0.125rem;
85 | border-top-right-radius: 0.125rem;
86 | }
87 | .list-group-item:last-child {
88 | border-bottom-left-radius: 0.125rem;
89 | border-bottom-right-radius: 0.125rem;
90 | }
91 |
92 | /** MISC **/
93 |
94 | .modifier {
95 | border-radius: 0.125rem;
96 | }
97 |
98 | pre[class*='language-'] {
99 | border-radius: 0.125rem;
100 | }
101 |
102 | /** TABLE **/
103 |
104 | .table-hover > tbody > tr:hover {
105 | background: rgba(0, 0, 0, 0.075);
106 | }
107 |
108 | table.params thead {
109 | background: none;
110 | }
111 | table.params thead td {
112 | color: rgba(0, 0, 0, 0.54);
113 | font-weight: bold;
114 | }
115 |
116 | @media (prefers-color-scheme: dark) {
117 | .menu .title:hover {
118 | background-color: #2d2d2d;
119 | }
120 | .menu ul.list li a:hover,
121 | .menu ul.list li.chapter .simple:hover {
122 | background-color: #2d2d2d;
123 | }
124 | .nav-tabs > li > a {
125 | color: #fafafa;
126 | }
127 | table.params thead {
128 | background: #484848;
129 | }
130 | table.params thead td {
131 | color: #fafafa;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/documentation/styles/original.css:
--------------------------------------------------------------------------------
1 | .navbar-default .navbar-brand,
2 | .menu ul.list li.title {
3 | font-weight: bold;
4 | color: #3c3c3c;
5 | padding-bottom: 5px;
6 | }
7 |
8 | .menu ul.list li a[data-type='chapter-link'],
9 | .menu ul.list li.chapter .simple {
10 | font-weight: bold;
11 | font-size: 14px;
12 | }
13 |
14 | .menu ul.list li a[href='./routes.html'] {
15 | border-bottom: none;
16 | }
17 |
18 | .menu ul.list > li:nth-child(2) {
19 | display: none;
20 | }
21 |
22 | .menu ul.list li.chapter ul.links {
23 | background: #fff;
24 | padding-left: 0;
25 | }
26 |
27 | .menu ul.list li.chapter ul.links li {
28 | border-bottom: 1px solid #ddd;
29 | padding-left: 20px;
30 | }
31 |
32 | .menu ul.list li.chapter ul.links li:last-child {
33 | border-bottom: none;
34 | }
35 |
36 | .menu ul.list li a.active {
37 | color: #337ab7;
38 | font-weight: bold;
39 | }
40 |
41 | #book-search-input {
42 | margin-bottom: 0;
43 | border-bottom: none;
44 | }
45 | .menu ul.list li.divider {
46 | margin: 0;
47 | }
48 |
49 | @media (prefers-color-scheme: dark) {
50 | .menu ul.list li.chapter ul.links {
51 | background: none;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/documentation/styles/readthedocs.css:
--------------------------------------------------------------------------------
1 | .navbar-default {
2 | background: #2980b9;
3 | border: none;
4 | }
5 |
6 | .navbar-default .navbar-brand {
7 | color: #fcfcfc;
8 | }
9 |
10 | .menu {
11 | background: #343131;
12 | color: #fcfcfc;
13 | }
14 |
15 | .menu ul.list li a {
16 | color: #fcfcfc;
17 | }
18 |
19 | .menu ul.list li.title {
20 | background: #2980b9;
21 | padding-bottom: 5px;
22 | }
23 |
24 | .menu ul.list li:nth-child(2) {
25 | margin-top: 0;
26 | }
27 |
28 | .menu ul.list li.chapter a,
29 | .menu ul.list li.chapter .simple {
30 | color: #555;
31 | text-transform: uppercase;
32 | text-decoration: none;
33 | }
34 |
35 | .menu ul.list li.chapter ul.links a {
36 | color: #b3b3b3;
37 | text-transform: none;
38 | padding-left: 35px;
39 | }
40 |
41 | .menu ul.list li.chapter ul.links a:hover {
42 | background: #4e4a4a;
43 | }
44 |
45 | .menu ul.list li.chapter a.active,
46 | .menu ul.list li.chapter ul.links a.active {
47 | color: #0099e5;
48 | }
49 |
50 | .menu ul.list li.chapter ul.links {
51 | padding-left: 0;
52 | }
53 |
54 | .menu ul.list li.divider {
55 | background: rgba(255, 255, 255, 0.07);
56 | }
57 |
58 | #book-search-input input,
59 | #book-search-input input:focus,
60 | #book-search-input input:hover {
61 | color: #949494;
62 | }
63 |
64 | .copyright {
65 | color: #b3b3b3;
66 | background: #272525;
67 | }
68 |
69 | .content {
70 | background: #fcfcfc;
71 | }
72 |
73 | .content a {
74 | color: #2980b9;
75 | }
76 |
77 | .content a:hover {
78 | color: #3091d1;
79 | }
80 |
81 | .content a:visited {
82 | color: #9b59b6;
83 | }
84 |
85 | .menu ul.list li:nth-last-child(2) {
86 | background: none;
87 | }
88 |
89 | code {
90 | white-space: nowrap;
91 | max-width: 100%;
92 | background: #fff;
93 | padding: 2px 5px;
94 | color: #e74c3c;
95 | overflow-x: auto;
96 | border-radius: 0;
97 | }
98 |
99 | pre {
100 | white-space: pre;
101 | margin: 0;
102 | padding: 12px 12px;
103 | font-size: 12px;
104 | line-height: 1.5;
105 | display: block;
106 | overflow: auto;
107 | color: #404040;
108 | background: rgba(238, 238, 238, 0.35);
109 | }
110 |
111 | @media (prefers-color-scheme: dark) {
112 | .content {
113 | background: none;
114 | }
115 | code {
116 | background: none;
117 | color: #e09393;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/documentation/styles/reset.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html, body, div, span, applet, object, iframe,
7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 | a, abbr, acronym, address, big, cite, code,
9 | del, dfn, em, img, ins, kbd, q, s, samp,
10 | small, strike, strong, sub, sup, tt, var,
11 | b, u, i, center,
12 | dl, dt, dd, ol, ul, li,
13 | fieldset, form, label, legend,
14 | table, caption, tbody, tfoot, thead, tr, th, td,
15 | article, aside, canvas, details, embed,
16 | figure, figcaption, footer, header, hgroup,
17 | menu, nav, output, ruby, section, summary,
18 | time, mark, audio, video {
19 | margin: 0;
20 | padding: 0;
21 | border: 0;
22 | font-size: 100%;
23 | font: inherit;
24 | vertical-align: baseline;
25 | }
26 | /* HTML5 display-role reset for older browsers */
27 | article, aside, details, figcaption, figure,
28 | footer, header, hgroup, menu, nav, section {
29 | display: block;
30 | }
31 | body {
32 | line-height: 1;
33 | }
34 | ol, ul {
35 | list-style: none;
36 | }
37 | blockquote, q {
38 | quotes: none;
39 | }
40 | blockquote:before, blockquote:after,
41 | q:before, q:after {
42 | content: '';
43 | content: none;
44 | }
45 | table {
46 | border-collapse: collapse;
47 | border-spacing: 0;
48 | }
--------------------------------------------------------------------------------
/documentation/styles/stripe.css:
--------------------------------------------------------------------------------
1 | .navbar-default .navbar-brand {
2 | color: #0099e5;
3 | }
4 |
5 | .menu ul.list li a[data-type='chapter-link'],
6 | .menu ul.list li.chapter .simple {
7 | color: #939da3;
8 | text-transform: uppercase;
9 | }
10 |
11 | .content h1,
12 | .content h2,
13 | .content h3,
14 | .content h4,
15 | .content h5 {
16 | color: #292e31;
17 | font-weight: normal;
18 | }
19 |
20 | .content {
21 | color: #4c555a;
22 | }
23 |
24 | .menu ul.list li.title {
25 | padding: 5px 0;
26 | }
27 |
28 | a {
29 | color: #0099e5;
30 | text-decoration: none;
31 | }
32 | a:hover {
33 | color: #292e31;
34 | text-decoration: none;
35 | }
36 |
37 | .menu ul.list li:nth-child(2) {
38 | margin-top: 0;
39 | }
40 |
41 | .menu ul.list li.title a,
42 | .navbar a {
43 | color: #0099e5;
44 | text-decoration: none;
45 | font-size: 16px;
46 | }
47 |
48 | .menu ul.list li a.active {
49 | color: #0099e5;
50 | }
51 |
52 | code {
53 | box-sizing: border-box;
54 | display: inline-block;
55 | padding: 0 5px;
56 | background: #fafcfc;
57 | border-radius: 4px;
58 | color: #b93d6a;
59 | font-size: 13px;
60 | line-height: 20px;
61 | }
62 |
63 | pre {
64 | margin: 0;
65 | padding: 12px 12px;
66 | background: #272b2d;
67 | border-radius: 5px;
68 | font-size: 13px;
69 | line-height: 1.5em;
70 | font-weight: 500;
71 | }
72 |
73 | @media (prefers-color-scheme: dark) {
74 | body {
75 | color: #fafafa;
76 | }
77 | .content h1,
78 | .content h2,
79 | .content h3,
80 | .content h4,
81 | .content h5 {
82 | color: #fafafa;
83 | }
84 |
85 | code {
86 | background: none;
87 | }
88 |
89 | .content {
90 | color: #fafafa;
91 | }
92 |
93 | .menu ul.list li a[data-type='chapter-link'],
94 | .menu ul.list li.chapter .simple {
95 | color: #fafafa;
96 | }
97 |
98 | .menu ul.list li.title a {
99 | color: #fafafa;
100 | }
101 |
102 | .menu ul.list li a {
103 | color: #fafafa;
104 | }
105 | .menu ul.list li a.active {
106 | color: #7fc9ff;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/documentation/styles/style.css:
--------------------------------------------------------------------------------
1 | @import "./reset.css";
2 | @import "./bootstrap.min.css";
3 | @import "./bootstrap-card.css";
4 | @import "./prism.css";
5 | @import "./ionicons.min.css";
6 | @import "./compodoc.css";
7 | @import "./tablesort.css";
8 |
--------------------------------------------------------------------------------
/documentation/styles/tablesort.css:
--------------------------------------------------------------------------------
1 | th[role=columnheader]:not(.no-sort) {
2 | cursor: pointer;
3 | }
4 |
5 | th[role=columnheader]:not(.no-sort):after {
6 | content: '';
7 | float: right;
8 | margin-top: 7px;
9 | border-width: 0 4px 4px;
10 | border-style: solid;
11 | border-color: #404040 transparent;
12 | visibility: visible;
13 | opacity: 1;
14 | -ms-user-select: none;
15 | -webkit-user-select: none;
16 | -moz-user-select: none;
17 | user-select: none;
18 | }
19 |
20 | th[aria-sort=ascending]:not(.no-sort):after {
21 | border-bottom: none;
22 | border-width: 4px 4px 0;
23 | }
24 |
25 | th[aria-sort]:not(.no-sort):after {
26 | visibility: visible;
27 | opacity: 0.4;
28 | }
29 |
30 | th[role=columnheader]:not(.no-sort):hover:after {
31 | visibility: visible;
32 | opacity: 1;
33 | }
34 |
--------------------------------------------------------------------------------
/documentation/styles/vagrant.css:
--------------------------------------------------------------------------------
1 | .navbar-default .navbar-brand {
2 | background: white;
3 | color: #8d9ba8;
4 | }
5 |
6 | .menu .list {
7 | background: #0c5593;
8 | }
9 |
10 | .menu .chapter {
11 | padding: 0 20px;
12 | }
13 |
14 | .menu ul.list li a[data-type='chapter-link'],
15 | .menu ul.list li.chapter .simple {
16 | color: white;
17 | text-transform: uppercase;
18 | border-bottom: 1px solid rgba(255, 255, 255, 0.4);
19 | }
20 |
21 | .content h1,
22 | .content h2,
23 | .content h3,
24 | .content h4,
25 | .content h5 {
26 | color: #292e31;
27 | font-weight: normal;
28 | }
29 |
30 | .content {
31 | color: #4c555a;
32 | }
33 |
34 | a {
35 | color: #0094bf;
36 | text-decoration: underline;
37 | }
38 | a:hover {
39 | color: #f1362f;
40 | }
41 |
42 | .menu ul.list li.title {
43 | background: white;
44 | padding-bottom: 5px;
45 | }
46 |
47 | .menu ul.list li:nth-child(2) {
48 | margin-top: 0;
49 | }
50 |
51 | .menu ul.list li:nth-last-child(2) {
52 | background: none;
53 | }
54 |
55 | .menu ul.list li.title a {
56 | padding: 10px 15px;
57 | }
58 |
59 | .menu ul.list li.title a,
60 | .navbar a {
61 | color: #8d9ba8;
62 | text-decoration: none;
63 | font-size: 16px;
64 | font-weight: 300;
65 | }
66 |
67 | .menu ul.list li a {
68 | color: white;
69 | padding: 10px;
70 | font-weight: 300;
71 | text-decoration: none;
72 | }
73 | .menu ul.list li a.active {
74 | color: white;
75 | font-weight: bold;
76 | }
77 |
78 | .copyright {
79 | color: white;
80 | background: #000;
81 | }
82 |
83 | code {
84 | box-sizing: border-box;
85 | display: inline-block;
86 | padding: 0 5px;
87 | background: rgba(0, 148, 191, 0.1);
88 | border-radius: 3px;
89 | color: #0094bf;
90 | font-size: 13px;
91 | line-height: 20px;
92 | }
93 |
94 | pre {
95 | margin: 0;
96 | padding: 12px 12px;
97 | background: rgba(238, 238, 238, 0.35);
98 | border-radius: 3px;
99 | font-size: 13px;
100 | line-height: 1.5em;
101 | font-weight: 500;
102 | }
103 |
104 | @media (prefers-color-scheme: dark) {
105 | body {
106 | color: #fafafa;
107 | }
108 | .content h1,
109 | .content h2,
110 | .content h3,
111 | .content h4,
112 | .content h5 {
113 | color: #fafafa;
114 | }
115 |
116 | code {
117 | background: none;
118 | }
119 |
120 | .content {
121 | color: #fafafa;
122 | }
123 |
124 | .menu ul.list li.title a,
125 | .navbar a {
126 | color: #8d9ba8;
127 | }
128 |
129 | .menu ul.list li a {
130 | color: #fafafa;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src",
4 | "compilerOptions": {
5 | "plugins": ["@nestjs/swagger"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/authentication/authentication.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AuthenticationService } from './authentication.service';
3 | import { UsersModule } from '../users/users.module';
4 | import { AuthenticationController } from './authentication.controller';
5 | import { PassportModule } from '@nestjs/passport';
6 | import { LocalStrategy } from './local.strategy';
7 | import { JwtModule } from '@nestjs/jwt';
8 | import { ConfigModule } from '@nestjs/config';
9 | import { JwtStrategy } from './jwt.strategy';
10 | import { JwtRefreshTokenStrategy } from './jwt-refresh-token.strategy';
11 | import { TwoFactorAuthenticationController } from './twoFactor/twoFactorAuthentication.controller';
12 | import { TwoFactorAuthenticationService } from './twoFactor/twoFactorAuthentication.service';
13 | import { JwtTwoFactorStrategy } from './jwt-two-factor.strategy';
14 | import { EmailConfirmationModule } from '../emailConfirmation/emailConfirmation.module';
15 |
16 | @Module({
17 | imports: [
18 | UsersModule,
19 | PassportModule,
20 | ConfigModule,
21 | JwtModule.register({}),
22 | EmailConfirmationModule,
23 | ],
24 | providers: [
25 | AuthenticationService,
26 | LocalStrategy,
27 | JwtStrategy,
28 | JwtRefreshTokenStrategy,
29 | TwoFactorAuthenticationService,
30 | JwtTwoFactorStrategy,
31 | ],
32 | controllers: [AuthenticationController, TwoFactorAuthenticationController],
33 | exports: [AuthenticationService],
34 | })
35 | export class AuthenticationModule {}
36 |
--------------------------------------------------------------------------------
/src/authentication/dto/logIn.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsEmail, IsString, IsNotEmpty, MinLength } from 'class-validator';
2 |
3 | export class LogInDto {
4 | @IsEmail()
5 | email: string;
6 |
7 | @IsString()
8 | @IsNotEmpty()
9 | @MinLength(7)
10 | password: string;
11 | }
12 |
13 | export default LogInDto;
14 |
--------------------------------------------------------------------------------
/src/authentication/dto/register.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsEmail,
3 | IsString,
4 | IsNotEmpty,
5 | MinLength,
6 | Matches,
7 | } from 'class-validator';
8 | import { ApiProperty } from '@nestjs/swagger';
9 |
10 | export class RegisterDto {
11 | @IsEmail()
12 | email: string;
13 |
14 | @IsString()
15 | @IsNotEmpty()
16 | name: string;
17 |
18 | @ApiProperty({
19 | deprecated: true,
20 | description: 'Use the name property instead',
21 | })
22 | fullName: string;
23 |
24 | @IsString()
25 | @IsNotEmpty()
26 | @MinLength(7)
27 | password: string;
28 |
29 | @ApiProperty({
30 | description: 'Has to match a regular expression: /^\\+[1-9]\\d{1,14}$/',
31 | example: '+123123123123',
32 | })
33 | @IsString()
34 | @IsNotEmpty()
35 | @Matches(/^\+[1-9]\d{1,14}$/)
36 | phoneNumber: string;
37 | }
38 |
39 | export default RegisterDto;
40 |
--------------------------------------------------------------------------------
/src/authentication/graphql-jwt-auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { AuthGuard } from '@nestjs/passport';
2 | import { ExecutionContext, Injectable } from '@nestjs/common';
3 | import { GqlExecutionContext } from '@nestjs/graphql';
4 |
5 | @Injectable()
6 | export class GraphqlJwtAuthGuard extends AuthGuard('jwt') {
7 | getRequest(context: ExecutionContext) {
8 | const ctx = GqlExecutionContext.create(context);
9 | return ctx.getContext().req;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/authentication/jwt-authentication.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 |
4 | @Injectable()
5 | export default class JwtAuthenticationGuard extends AuthGuard('jwt') {}
6 |
--------------------------------------------------------------------------------
/src/authentication/jwt-refresh-token.strategy.ts:
--------------------------------------------------------------------------------
1 | import { ExtractJwt, Strategy } from 'passport-jwt';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { Injectable } from '@nestjs/common';
4 | import { ConfigService } from '@nestjs/config';
5 | import { Request } from 'express';
6 | import { UsersService } from '../users/users.service';
7 | import TokenPayload from './tokenPayload.interface';
8 |
9 | @Injectable()
10 | export class JwtRefreshTokenStrategy extends PassportStrategy(
11 | Strategy,
12 | 'jwt-refresh-token',
13 | ) {
14 | constructor(
15 | private readonly configService: ConfigService,
16 | private readonly userService: UsersService,
17 | ) {
18 | super({
19 | jwtFromRequest: ExtractJwt.fromExtractors([
20 | (request: Request) => {
21 | return request?.cookies?.Refresh;
22 | },
23 | ]),
24 | secretOrKey: configService.get('JWT_REFRESH_TOKEN_SECRET'),
25 | passReqToCallback: true,
26 | });
27 | }
28 |
29 | async validate(request: Request, payload: TokenPayload) {
30 | const refreshToken = request.cookies?.Refresh;
31 | return this.userService.getUserIfRefreshTokenMatches(
32 | refreshToken,
33 | payload.userId,
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/authentication/jwt-refresh.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 |
4 | @Injectable()
5 | export default class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') {}
6 |
--------------------------------------------------------------------------------
/src/authentication/jwt-two-factor.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 |
4 | @Injectable()
5 | export default class JwtTwoFactorGuard extends AuthGuard('jwt-two-factor') {}
6 |
--------------------------------------------------------------------------------
/src/authentication/jwt-two-factor.strategy.ts:
--------------------------------------------------------------------------------
1 | import { ExtractJwt, Strategy } from 'passport-jwt';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { Injectable } from '@nestjs/common';
4 | import { ConfigService } from '@nestjs/config';
5 | import { Request } from 'express';
6 | import { UsersService } from '../users/users.service';
7 | import TokenPayload from './tokenPayload.interface';
8 |
9 | @Injectable()
10 | export class JwtTwoFactorStrategy extends PassportStrategy(
11 | Strategy,
12 | 'jwt-two-factor',
13 | ) {
14 | constructor(
15 | private readonly configService: ConfigService,
16 | private readonly userService: UsersService,
17 | ) {
18 | super({
19 | jwtFromRequest: ExtractJwt.fromExtractors([
20 | (request: Request) => {
21 | return request?.cookies?.Authentication;
22 | },
23 | ]),
24 | secretOrKey: configService.get('JWT_ACCESS_TOKEN_SECRET'),
25 | });
26 | }
27 |
28 | async validate(payload: TokenPayload) {
29 | const user = await this.userService.getById(payload.userId);
30 | if (!user.isTwoFactorAuthenticationEnabled) {
31 | return user;
32 | }
33 | if (payload.isSecondFactorAuthenticated) {
34 | return user;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/authentication/jwt.strategy.ts:
--------------------------------------------------------------------------------
1 | import { ExtractJwt, Strategy } from 'passport-jwt';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { Injectable } from '@nestjs/common';
4 | import { ConfigService } from '@nestjs/config';
5 | import { Request } from 'express';
6 | import { UsersService } from '../users/users.service';
7 | import TokenPayload from './tokenPayload.interface';
8 |
9 | @Injectable()
10 | export class JwtStrategy extends PassportStrategy(Strategy) {
11 | constructor(
12 | private readonly configService: ConfigService,
13 | private readonly userService: UsersService,
14 | ) {
15 | super({
16 | jwtFromRequest: ExtractJwt.fromExtractors([
17 | (request: Request) => {
18 | return request?.cookies?.Authentication;
19 | },
20 | ]),
21 | secretOrKey: configService.get('JWT_ACCESS_TOKEN_SECRET'),
22 | });
23 | }
24 |
25 | async validate(payload: TokenPayload) {
26 | return this.userService.getById(payload.userId);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/authentication/local.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Strategy } from 'passport-local';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { Injectable } from '@nestjs/common';
4 | import { AuthenticationService } from './authentication.service';
5 | import User from '../users/user.entity';
6 |
7 | @Injectable()
8 | export class LocalStrategy extends PassportStrategy(Strategy) {
9 | constructor(private authenticationService: AuthenticationService) {
10 | super({
11 | usernameField: 'email',
12 | });
13 | }
14 | async validate(email: string, password: string): Promise {
15 | return this.authenticationService.getAuthenticatedUser(email, password);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/authentication/localAuthentication.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 |
4 | @Injectable()
5 | export class LocalAuthenticationGuard extends AuthGuard('local') {}
6 |
--------------------------------------------------------------------------------
/src/authentication/requestWithUser.interface.ts:
--------------------------------------------------------------------------------
1 | import { Request } from 'express';
2 | import User from '../users/user.entity';
3 |
4 | interface RequestWithUser extends Request {
5 | user: User;
6 | }
7 |
8 | export default RequestWithUser;
9 |
--------------------------------------------------------------------------------
/src/authentication/tests/authentication.controller.integration-spec.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticationService } from '../authentication.service';
2 | import { Test } from '@nestjs/testing';
3 | import { ConfigService } from '@nestjs/config';
4 | import { JwtService } from '@nestjs/jwt';
5 | import { getRepositoryToken } from '@nestjs/typeorm';
6 | import User from '../../users/user.entity';
7 | import { UsersService } from '../../users/users.service';
8 | import mockedJwtService from '../../utils/mocks/jwt.service';
9 | import mockedConfigService from '../../utils/mocks/config.service';
10 | import { AuthenticationController } from '../authentication.controller';
11 | import { INestApplication, ValidationPipe } from '@nestjs/common';
12 | import * as request from 'supertest';
13 | import mockedUser from './user.mock';
14 |
15 | describe('The AuthenticationController', () => {
16 | let app: INestApplication;
17 | let userData: User;
18 | beforeEach(async () => {
19 | userData = {
20 | ...mockedUser,
21 | };
22 | const usersRepository = {
23 | create: jest.fn().mockResolvedValue(userData),
24 | save: jest.fn().mockReturnValue(Promise.resolve()),
25 | };
26 |
27 | const module = await Test.createTestingModule({
28 | controllers: [AuthenticationController],
29 | providers: [
30 | UsersService,
31 | AuthenticationService,
32 | {
33 | provide: ConfigService,
34 | useValue: mockedConfigService,
35 | },
36 | {
37 | provide: JwtService,
38 | useValue: mockedJwtService,
39 | },
40 | {
41 | provide: getRepositoryToken(User),
42 | useValue: usersRepository,
43 | },
44 | ],
45 | }).compile();
46 | app = module.createNestApplication();
47 | app.useGlobalPipes(new ValidationPipe());
48 | await app.init();
49 | });
50 | describe('when registering', () => {
51 | describe('and using valid data', () => {
52 | it('should respond with the data of the user without the password', () => {
53 | const expectedData = {
54 | ...userData,
55 | };
56 | delete expectedData.password;
57 | return request(app.getHttpServer())
58 | .post('/authentication/register')
59 | .send({
60 | email: mockedUser.email,
61 | name: mockedUser.name,
62 | password: 'strongPassword',
63 | })
64 | .expect(201)
65 | .expect(expectedData);
66 | });
67 | });
68 | describe('and using invalid data', () => {
69 | it('should throw an error', () => {
70 | return request(app.getHttpServer())
71 | .post('/authentication/register')
72 | .send({
73 | name: mockedUser.name,
74 | })
75 | .expect(400);
76 | });
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/src/authentication/tests/authentication.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticationService } from '../authentication.service';
2 | import { Test } from '@nestjs/testing';
3 | import { ConfigService } from '@nestjs/config';
4 | import { JwtService } from '@nestjs/jwt';
5 | import { getRepositoryToken } from '@nestjs/typeorm';
6 | import User from '../../users/user.entity';
7 | import { UsersService } from '../../users/users.service';
8 | import mockedJwtService from '../../utils/mocks/jwt.service';
9 | import mockedConfigService from '../../utils/mocks/config.service';
10 |
11 | describe('The AuthenticationService', () => {
12 | let authenticationService: AuthenticationService;
13 | beforeEach(async () => {
14 | const module = await Test.createTestingModule({
15 | providers: [
16 | UsersService,
17 | AuthenticationService,
18 | {
19 | provide: ConfigService,
20 | useValue: mockedConfigService,
21 | },
22 | {
23 | provide: JwtService,
24 | useValue: mockedJwtService,
25 | },
26 | {
27 | provide: getRepositoryToken(User),
28 | useValue: {},
29 | },
30 | ],
31 | }).compile();
32 | authenticationService = await module.get(AuthenticationService);
33 | });
34 | describe('when creating a cookie', () => {
35 | it('should return a string', () => {
36 | const userId = 1;
37 | expect(
38 | typeof authenticationService.getCookieWithJwtToken(userId),
39 | ).toEqual('string');
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/src/authentication/tests/user.mock.ts:
--------------------------------------------------------------------------------
1 | import User from '../../users/user.entity';
2 |
3 | const mockedUser: User = {
4 | id: 1,
5 | email: 'user@email.com',
6 | name: 'John',
7 | password: 'hash',
8 | stripeCustomerId: 'stripe_customer_id',
9 | phoneNumber: '+48123123123',
10 | address: {
11 | id: 1,
12 | street: 'streetName',
13 | city: 'cityName',
14 | country: 'countryName',
15 | },
16 | isTwoFactorAuthenticationEnabled: false,
17 | isEmailConfirmed: false,
18 | isPhoneNumberConfirmed: false,
19 | isRegisteredWithGoogle: false,
20 | };
21 |
22 | export default mockedUser;
23 |
--------------------------------------------------------------------------------
/src/authentication/tokenPayload.interface.ts:
--------------------------------------------------------------------------------
1 | interface TokenPayload {
2 | userId: number;
3 | isSecondFactorAuthenticated?: boolean;
4 | }
5 |
6 | export default TokenPayload;
7 |
--------------------------------------------------------------------------------
/src/authentication/twoFactor/dto/twoFactorAuthenticationCode.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNotEmpty, IsString } from 'class-validator';
2 |
3 | export class TwoFactorAuthenticationCodeDto {
4 | @IsString()
5 | @IsNotEmpty()
6 | twoFactorAuthenticationCode: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/authentication/twoFactor/twoFactorAuthentication.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ClassSerializerInterceptor,
3 | Controller,
4 | Post,
5 | UseInterceptors,
6 | Res,
7 | UseGuards,
8 | Req,
9 | Body,
10 | UnauthorizedException,
11 | HttpCode,
12 | } from '@nestjs/common';
13 | import { TwoFactorAuthenticationService } from './twoFactorAuthentication.service';
14 | import { Response } from 'express';
15 | import JwtAuthenticationGuard from '../jwt-authentication.guard';
16 | import RequestWithUser from '../requestWithUser.interface';
17 | import { UsersService } from '../../users/users.service';
18 | import { TwoFactorAuthenticationCodeDto } from './dto/twoFactorAuthenticationCode.dto';
19 | import { AuthenticationService } from '../authentication.service';
20 |
21 | @Controller('2fa')
22 | @UseInterceptors(ClassSerializerInterceptor)
23 | export class TwoFactorAuthenticationController {
24 | constructor(
25 | private readonly twoFactorAuthenticationService: TwoFactorAuthenticationService,
26 | private readonly usersService: UsersService,
27 | private readonly authenticationService: AuthenticationService,
28 | ) {}
29 |
30 | @Post('generate')
31 | @UseGuards(JwtAuthenticationGuard)
32 | async register(@Res() response: Response, @Req() request: RequestWithUser) {
33 | const {
34 | otpauthUrl,
35 | } = await this.twoFactorAuthenticationService.generateTwoFactorAuthenticationSecret(
36 | request.user,
37 | );
38 |
39 | return this.twoFactorAuthenticationService.pipeQrCodeStream(
40 | response,
41 | otpauthUrl,
42 | );
43 | }
44 |
45 | @Post('turn-on')
46 | @HttpCode(200)
47 | @UseGuards(JwtAuthenticationGuard)
48 | async turnOnTwoFactorAuthentication(
49 | @Req() request: RequestWithUser,
50 | @Body() { twoFactorAuthenticationCode }: TwoFactorAuthenticationCodeDto,
51 | ) {
52 | const isCodeValid = this.twoFactorAuthenticationService.isTwoFactorAuthenticationCodeValid(
53 | twoFactorAuthenticationCode,
54 | request.user,
55 | );
56 | if (!isCodeValid) {
57 | throw new UnauthorizedException('Wrong authentication code');
58 | }
59 | await this.usersService.turnOnTwoFactorAuthentication(request.user.id);
60 | }
61 |
62 | @Post('authenticate')
63 | @HttpCode(200)
64 | @UseGuards(JwtAuthenticationGuard)
65 | async authenticate(
66 | @Req() request: RequestWithUser,
67 | @Body() { twoFactorAuthenticationCode }: TwoFactorAuthenticationCodeDto,
68 | ) {
69 | const isCodeValid = this.twoFactorAuthenticationService.isTwoFactorAuthenticationCodeValid(
70 | twoFactorAuthenticationCode,
71 | request.user,
72 | );
73 | if (!isCodeValid) {
74 | throw new UnauthorizedException('Wrong authentication code');
75 | }
76 |
77 | const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken(
78 | request.user.id,
79 | true,
80 | );
81 |
82 | request.res.setHeader('Set-Cookie', [accessTokenCookie]);
83 |
84 | return request.user;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/authentication/twoFactor/twoFactorAuthentication.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { authenticator } from 'otplib';
3 | import { toFileStream } from 'qrcode';
4 | import { Response } from 'express';
5 | import User from '../../users/user.entity';
6 | import { UsersService } from '../../users/users.service';
7 | import { ConfigService } from '@nestjs/config';
8 |
9 | @Injectable()
10 | export class TwoFactorAuthenticationService {
11 | constructor(
12 | private readonly usersService: UsersService,
13 | private readonly configService: ConfigService,
14 | ) {}
15 |
16 | public async generateTwoFactorAuthenticationSecret(user: User) {
17 | const secret = authenticator.generateSecret();
18 |
19 | const otpauthUrl = authenticator.keyuri(
20 | user.email,
21 | this.configService.get('TWO_FACTOR_AUTHENTICATION_APP_NAME'),
22 | secret,
23 | );
24 |
25 | await this.usersService.setTwoFactorAuthenticationSecret(secret, user.id);
26 |
27 | return {
28 | secret,
29 | otpauthUrl,
30 | };
31 | }
32 |
33 | public isTwoFactorAuthenticationCodeValid(
34 | twoFactorAuthenticationCode: string,
35 | user: User,
36 | ) {
37 | return authenticator.verify({
38 | token: twoFactorAuthenticationCode,
39 | secret: user.twoFactorAuthenticationSecret,
40 | });
41 | }
42 |
43 | public async pipeQrCodeStream(stream: Response, otpauthUrl: string) {
44 | return toFileStream(stream, otpauthUrl);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/categories/categories.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | Param,
7 | Patch,
8 | UseGuards,
9 | UseInterceptors,
10 | ClassSerializerInterceptor,
11 | Post,
12 | } from '@nestjs/common';
13 | import CategoriesService from './categories.service';
14 | import CreateCategoryDto from './dto/createCategory.dto';
15 | import UpdateCategoryDto from './dto/updateCategory.dto';
16 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
17 | import FindOneParams from '../utils/findOneParams';
18 |
19 | @Controller('categories')
20 | @UseInterceptors(ClassSerializerInterceptor)
21 | export default class CategoriesController {
22 | constructor(private readonly categoriesService: CategoriesService) {}
23 |
24 | @Get()
25 | getAllCategories() {
26 | return this.categoriesService.getAllCategories();
27 | }
28 |
29 | @Get(':id')
30 | getCategoryById(@Param() { id }: FindOneParams) {
31 | return this.categoriesService.getCategoryById(Number(id));
32 | }
33 |
34 | @Post()
35 | @UseGuards(JwtAuthenticationGuard)
36 | async createCategory(@Body() category: CreateCategoryDto) {
37 | return this.categoriesService.createCategory(category);
38 | }
39 |
40 | @Patch(':id')
41 | async updateCategory(
42 | @Param() { id }: FindOneParams,
43 | @Body() category: UpdateCategoryDto,
44 | ) {
45 | return this.categoriesService.updateCategory(Number(id), category);
46 | }
47 |
48 | @Delete(':id')
49 | async deleteCategory(@Param() { id }: FindOneParams) {
50 | return this.categoriesService.deleteCategory(Number(id));
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/categories/categories.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import CategoriesController from './categories.controller';
3 | import CategoriesService from './categories.service';
4 | import Category from './category.entity';
5 | import { TypeOrmModule } from '@nestjs/typeorm';
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([Category])],
9 | controllers: [CategoriesController],
10 | providers: [CategoriesService],
11 | })
12 | export class CategoriesModule {}
13 |
--------------------------------------------------------------------------------
/src/categories/categories.service.md:
--------------------------------------------------------------------------------
1 | # CategoriesService
2 |
3 | This service aims to perform various operations on the entity of the category.
--------------------------------------------------------------------------------
/src/categories/category.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | DeleteDateColumn,
4 | Entity,
5 | ManyToMany,
6 | PrimaryGeneratedColumn,
7 | } from 'typeorm';
8 | import Post from '../posts/post.entity';
9 |
10 | @Entity()
11 | class Category {
12 | @PrimaryGeneratedColumn()
13 | public id: number;
14 |
15 | @Column()
16 | public name: string;
17 |
18 | @ManyToMany(
19 | () => Post,
20 | (post: Post) => post.categories,
21 | )
22 | public posts: Post[];
23 |
24 | @DeleteDateColumn()
25 | public deletedAt: Date;
26 | }
27 |
28 | export default Category;
29 |
--------------------------------------------------------------------------------
/src/categories/dto/createCategory.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty } from 'class-validator';
2 |
3 | export class CreateCategoryDto {
4 | @IsString()
5 | @IsNotEmpty()
6 | name: string;
7 | }
8 |
9 | export default CreateCategoryDto;
10 |
--------------------------------------------------------------------------------
/src/categories/dto/updateCategory.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty, IsNumber, IsOptional } from 'class-validator';
2 |
3 | export class UpdateCategoryDto {
4 | @IsNumber()
5 | @IsOptional()
6 | id: number;
7 |
8 | @IsString()
9 | @IsNotEmpty()
10 | @IsOptional()
11 | name: string;
12 | }
13 |
14 | export default UpdateCategoryDto;
15 |
--------------------------------------------------------------------------------
/src/categories/exceptions/categoryNotFound.exception.ts:
--------------------------------------------------------------------------------
1 | import { NotFoundException } from '@nestjs/common';
2 |
3 | class CategoryNotFoundException extends NotFoundException {
4 | constructor(postId: number) {
5 | super(`Category with id ${postId} not found`);
6 | }
7 | }
8 |
9 | export default CategoryNotFoundException;
10 |
--------------------------------------------------------------------------------
/src/charge/charge.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common';
2 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
3 | import CreateChargeDto from './dto/createCharge.dto';
4 | import RequestWithUser from '../authentication/requestWithUser.interface';
5 | import StripeService from '../stripe/stripe.service';
6 |
7 | @Controller('charge')
8 | export default class ChargeController {
9 | constructor(private readonly stripeService: StripeService) {}
10 |
11 | @Post()
12 | @UseGuards(JwtAuthenticationGuard)
13 | async createCharge(
14 | @Body() charge: CreateChargeDto,
15 | @Req() request: RequestWithUser,
16 | ) {
17 | return this.stripeService.charge(
18 | charge.amount,
19 | charge.paymentMethodId,
20 | request.user.stripeCustomerId,
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/charge/charge.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { StripeModule } from '../stripe/stripe.module';
3 | import ChargeController from './charge.controller';
4 |
5 | @Module({
6 | imports: [StripeModule],
7 | controllers: [ChargeController],
8 | providers: [],
9 | })
10 | export class ChargeModule {}
11 |
--------------------------------------------------------------------------------
/src/charge/dto/createCharge.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty, IsNumber } from 'class-validator';
2 |
3 | export class CreateChargeDto {
4 | @IsString()
5 | @IsNotEmpty()
6 | paymentMethodId: string;
7 |
8 | @IsNumber()
9 | amount: number;
10 | }
11 |
12 | export default CreateChargeDto;
13 |
--------------------------------------------------------------------------------
/src/chat/chat.gateway.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ConnectedSocket,
3 | MessageBody,
4 | OnGatewayConnection,
5 | SubscribeMessage,
6 | WebSocketGateway,
7 | WebSocketServer,
8 | } from '@nestjs/websockets';
9 | import { Server, Socket } from 'socket.io';
10 | import { ChatService } from './chat.service';
11 |
12 | @WebSocketGateway()
13 | export class ChatGateway implements OnGatewayConnection {
14 | @WebSocketServer()
15 | server: Server;
16 |
17 | constructor(private readonly chatService: ChatService) {}
18 |
19 | async handleConnection(socket: Socket) {
20 | await this.chatService.getUserFromSocket(socket);
21 | }
22 |
23 | @SubscribeMessage('send_message')
24 | async listenForMessages(
25 | @MessageBody() content: string,
26 | @ConnectedSocket() socket: Socket,
27 | ) {
28 | const author = await this.chatService.getUserFromSocket(socket);
29 | const message = await this.chatService.saveMessage(content, author);
30 |
31 | this.server.sockets.emit('receive_message', message);
32 | }
33 |
34 | @SubscribeMessage('request_all_messages')
35 | async requestAllMessages(@ConnectedSocket() socket: Socket) {
36 | await this.chatService.getUserFromSocket(socket);
37 | const messages = await this.chatService.getAllMessages();
38 |
39 | socket.emit('send_all_messages', messages);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/chat/chat.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ChatGateway } from './chat.gateway';
3 | import { AuthenticationModule } from '../authentication/authentication.module';
4 | import { ChatService } from './chat.service';
5 | import { TypeOrmModule } from '@nestjs/typeorm';
6 | import Message from './message.entity';
7 |
8 | @Module({
9 | imports: [AuthenticationModule, TypeOrmModule.forFeature([Message])],
10 | controllers: [],
11 | providers: [ChatGateway, ChatService],
12 | })
13 | export class ChatModule {}
14 |
--------------------------------------------------------------------------------
/src/chat/chat.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { AuthenticationService } from '../authentication/authentication.service';
3 | import { Socket } from 'socket.io';
4 | import { parse } from 'cookie';
5 | import { WsException } from '@nestjs/websockets';
6 | import { InjectRepository } from '@nestjs/typeorm';
7 | import Message from './message.entity';
8 | import User from '../users/user.entity';
9 | import { Repository } from 'typeorm';
10 |
11 | @Injectable()
12 | export class ChatService {
13 | constructor(
14 | private readonly authenticationService: AuthenticationService,
15 | @InjectRepository(Message)
16 | private messagesRepository: Repository,
17 | ) {}
18 |
19 | async saveMessage(content: string, author: User) {
20 | const newMessage = await this.messagesRepository.create({
21 | content,
22 | author,
23 | });
24 | await this.messagesRepository.save(newMessage);
25 | return newMessage;
26 | }
27 |
28 | async getAllMessages() {
29 | return this.messagesRepository.find({
30 | relations: {
31 | author: true,
32 | },
33 | });
34 | }
35 |
36 | async getUserFromSocket(socket: Socket) {
37 | const cookie = socket.handshake.headers.cookie;
38 | const { Authentication: authenticationToken } = parse(cookie);
39 | const user = await this.authenticationService.getUserFromAuthenticationToken(
40 | authenticationToken,
41 | );
42 | if (!user) {
43 | throw new WsException('Invalid credentials.');
44 | }
45 | return user;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/chat/message.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
2 | import User from '../users/user.entity';
3 |
4 | @Entity()
5 | class Message {
6 | @PrimaryGeneratedColumn()
7 | public id: number;
8 |
9 | @Column()
10 | public content: string;
11 |
12 | @ManyToOne(() => User)
13 | public author: User;
14 | }
15 |
16 | export default Message;
17 |
--------------------------------------------------------------------------------
/src/comments/commands/handlers/create-comment.handler.ts:
--------------------------------------------------------------------------------
1 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
2 | import { CreateCommentCommand } from '../implementations/createComment.command';
3 | import { InjectRepository } from '@nestjs/typeorm';
4 | import Comment from '../../comment.entity';
5 | import { Repository } from 'typeorm';
6 |
7 | @CommandHandler(CreateCommentCommand)
8 | export class CreateCommentHandler
9 | implements ICommandHandler {
10 | constructor(
11 | @InjectRepository(Comment)
12 | private commentsRepository: Repository,
13 | ) {}
14 |
15 | async execute(command: CreateCommentCommand) {
16 | const newPost = await this.commentsRepository.create({
17 | ...command.comment,
18 | author: command.author,
19 | });
20 | await this.commentsRepository.save(newPost);
21 | return newPost;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/comments/commands/implementations/createComment.command.ts:
--------------------------------------------------------------------------------
1 | import CreateCommentDto from '../../dto/createComment.dto';
2 | import User from '../../../users/user.entity';
3 |
4 | export class CreateCommentCommand {
5 | constructor(
6 | public readonly comment: CreateCommentDto,
7 | public readonly author: User,
8 | ) {}
9 | }
10 |
--------------------------------------------------------------------------------
/src/comments/comment.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
2 | import User from '../users/user.entity';
3 | import Post from '../posts/post.entity';
4 |
5 | @Entity()
6 | class Comment {
7 | @PrimaryGeneratedColumn()
8 | public id: number;
9 |
10 | @Column()
11 | public content: string;
12 |
13 | @ManyToOne(
14 | () => Post,
15 | (post: Post) => post.comments,
16 | )
17 | public post: Post;
18 |
19 | @ManyToOne(
20 | () => User,
21 | (author: User) => author.posts,
22 | )
23 | public author: User;
24 | }
25 |
26 | export default Comment;
27 |
--------------------------------------------------------------------------------
/src/comments/comments.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | ClassSerializerInterceptor,
4 | Controller,
5 | Get,
6 | Post,
7 | Query,
8 | Req,
9 | UseGuards,
10 | UseInterceptors,
11 | } from '@nestjs/common';
12 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
13 | import RequestWithUser from '../authentication/requestWithUser.interface';
14 | import CreateCommentDto from './dto/createComment.dto';
15 | import { CommandBus, QueryBus } from '@nestjs/cqrs';
16 | import { CreateCommentCommand } from './commands/implementations/createComment.command';
17 | import { GetCommentsQuery } from './queries/implementations/getComments.query';
18 | import GetCommentsDto from './dto/getComments.dto';
19 |
20 | @Controller('comments')
21 | @UseInterceptors(ClassSerializerInterceptor)
22 | export default class CommentsController {
23 | constructor(private commandBus: CommandBus, private queryBus: QueryBus) {}
24 |
25 | @Post()
26 | @UseGuards(JwtAuthenticationGuard)
27 | async createComment(
28 | @Body() comment: CreateCommentDto,
29 | @Req() req: RequestWithUser,
30 | ) {
31 | const user = req.user;
32 | return this.commandBus.execute(new CreateCommentCommand(comment, user));
33 | }
34 |
35 | @Get()
36 | async getComments(@Query() { postId }: GetCommentsDto) {
37 | return this.queryBus.execute(new GetCommentsQuery(postId));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/comments/comments.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import Comment from './comment.entity';
4 | import CommentsController from './comments.controller';
5 | import { CqrsModule } from '@nestjs/cqrs';
6 | import { CreateCommentHandler } from './commands/handlers/create-comment.handler';
7 | import { GetCommentsHandler } from './queries/handlers/getComments.handler';
8 |
9 | @Module({
10 | imports: [TypeOrmModule.forFeature([Comment]), CqrsModule],
11 | controllers: [CommentsController],
12 | providers: [CreateCommentHandler, GetCommentsHandler],
13 | })
14 | export class CommentsModule {}
15 |
--------------------------------------------------------------------------------
/src/comments/dto/createComment.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty, ValidateNested } from 'class-validator';
2 | import { Type } from 'class-transformer';
3 | import ObjectWithIdDTO from 'src/utils/types/objectWithId.dto';
4 |
5 | class CreateCommentDto {
6 | @IsString()
7 | @IsNotEmpty()
8 | content: string;
9 |
10 | @ValidateNested()
11 | @Type(() => ObjectWithIdDTO)
12 | post: ObjectWithIdDTO;
13 | }
14 |
15 | export default CreateCommentDto;
16 |
--------------------------------------------------------------------------------
/src/comments/dto/getComments.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsOptional } from 'class-validator';
2 | import { Type } from 'class-transformer';
3 |
4 | class GetCommentsDto {
5 | @Type(() => Number)
6 | @IsOptional()
7 | postId?: number;
8 | }
9 |
10 | export default GetCommentsDto;
11 |
--------------------------------------------------------------------------------
/src/comments/queries/handlers/getComments.handler.ts:
--------------------------------------------------------------------------------
1 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
2 | import { GetCommentsQuery } from '../implementations/getComments.query';
3 | import { InjectRepository } from '@nestjs/typeorm';
4 | import Comment from '../../comment.entity';
5 | import { Repository } from 'typeorm';
6 |
7 | @QueryHandler(GetCommentsQuery)
8 | export class GetCommentsHandler implements IQueryHandler {
9 | constructor(
10 | @InjectRepository(Comment)
11 | private commentsRepository: Repository,
12 | ) {}
13 |
14 | async execute(query: GetCommentsQuery) {
15 | if (query.postId) {
16 | return this.commentsRepository.findBy({
17 | post: {
18 | id: query.postId,
19 | },
20 | });
21 | }
22 | return this.commentsRepository.find();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/comments/queries/implementations/getComments.query.ts:
--------------------------------------------------------------------------------
1 | export class GetCommentsQuery {
2 | constructor(public readonly postId?: number) {}
3 | }
4 |
--------------------------------------------------------------------------------
/src/credit-cards/creditCards.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Post,
5 | Req,
6 | UseGuards,
7 | Get,
8 | HttpCode,
9 | } from '@nestjs/common';
10 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
11 | import RequestWithUser from '../authentication/requestWithUser.interface';
12 | import StripeService from '../stripe/stripe.service';
13 | import AddCreditCardDto from './dto/addCreditCardDto';
14 | import SetDefaultCreditCardDto from './dto/setDefaultCreditCard.dto';
15 | import { EmailConfirmationGuard } from '../emailConfirmation/emailConfirmation.guard';
16 |
17 | @Controller('credit-cards')
18 | export default class CreditCardsController {
19 | constructor(private readonly stripeService: StripeService) {}
20 |
21 | @Post()
22 | @UseGuards(JwtAuthenticationGuard)
23 | async addCreditCard(
24 | @Body() creditCard: AddCreditCardDto,
25 | @Req() request: RequestWithUser,
26 | ) {
27 | return this.stripeService.attachCreditCard(
28 | creditCard.paymentMethodId,
29 | request.user.stripeCustomerId,
30 | );
31 | }
32 |
33 | @Post('default')
34 | @HttpCode(200)
35 | @UseGuards(JwtAuthenticationGuard)
36 | async setDefaultCard(
37 | @Body() creditCard: SetDefaultCreditCardDto,
38 | @Req() request: RequestWithUser,
39 | ) {
40 | await this.stripeService.setDefaultCreditCard(
41 | creditCard.paymentMethodId,
42 | request.user.stripeCustomerId,
43 | );
44 | }
45 |
46 | @Get()
47 | @UseGuards(EmailConfirmationGuard)
48 | @UseGuards(JwtAuthenticationGuard)
49 | async getCreditCards(@Req() request: RequestWithUser) {
50 | return this.stripeService.listCreditCards(request.user.stripeCustomerId);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/credit-cards/creditCards.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { StripeModule } from '../stripe/stripe.module';
3 | import CreditCardsController from './creditCards.controller';
4 |
5 | @Module({
6 | imports: [StripeModule],
7 | controllers: [CreditCardsController],
8 | providers: [],
9 | })
10 | export class CreditCardsModule {}
11 |
--------------------------------------------------------------------------------
/src/credit-cards/dto/addCreditCardDto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty } from 'class-validator';
2 |
3 | export class AddCreditCardDto {
4 | @IsString()
5 | @IsNotEmpty()
6 | paymentMethodId: string;
7 | }
8 |
9 | export default AddCreditCardDto;
10 |
--------------------------------------------------------------------------------
/src/credit-cards/dto/setDefaultCreditCard.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty } from 'class-validator';
2 |
3 | export class SetDefaultCreditCardDto {
4 | @IsString()
5 | @IsNotEmpty()
6 | paymentMethodId: string;
7 | }
8 |
9 | export default SetDefaultCreditCardDto;
10 |
--------------------------------------------------------------------------------
/src/database/database.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { ConfigModule, ConfigService } from '@nestjs/config';
4 | import DatabaseLogger from './databaseLogger';
5 | import Address from '../users/address.entity';
6 |
7 | @Module({
8 | imports: [
9 | TypeOrmModule.forRootAsync({
10 | imports: [ConfigModule],
11 | inject: [ConfigService],
12 | useFactory: (configService: ConfigService) => ({
13 | type: 'postgres',
14 | logger: new DatabaseLogger(),
15 | host: configService.get('POSTGRES_HOST'),
16 | port: configService.get('POSTGRES_PORT'),
17 | username: configService.get('POSTGRES_USER'),
18 | password: configService.get('POSTGRES_PASSWORD'),
19 | database: configService.get('POSTGRES_DB'),
20 | entities: [Address],
21 | synchronize: true,
22 | autoLoadEntities: true,
23 | }),
24 | }),
25 | ],
26 | })
27 | export class DatabaseModule {}
28 |
--------------------------------------------------------------------------------
/src/database/databaseLogger.ts:
--------------------------------------------------------------------------------
1 | import { Logger as TypeOrmLogger, QueryRunner } from 'typeorm';
2 | import { Logger as NestLogger } from '@nestjs/common';
3 |
4 | class DatabaseLogger implements TypeOrmLogger {
5 | private readonly logger = new NestLogger('SQL');
6 |
7 | logQuery(query: string, parameters?: unknown[], queryRunner?: QueryRunner) {
8 | if (queryRunner?.data?.isCreatingLogs) {
9 | return;
10 | }
11 | this.logger.log(
12 | `${query} -- Parameters: ${this.stringifyParameters(parameters)}`,
13 | );
14 | }
15 | logQueryError(
16 | error: string,
17 | query: string,
18 | parameters?: unknown[],
19 | queryRunner?: QueryRunner,
20 | ) {
21 | if (queryRunner?.data?.isCreatingLogs) {
22 | return;
23 | }
24 | this.logger.error(
25 | `${query} -- Parameters: ${this.stringifyParameters(
26 | parameters,
27 | )} -- ${error}`,
28 | );
29 | }
30 | logQuerySlow(
31 | time: number,
32 | query: string,
33 | parameters?: unknown[],
34 | queryRunner?: QueryRunner,
35 | ) {
36 | if (queryRunner?.data?.isCreatingLogs) {
37 | return;
38 | }
39 | this.logger.warn(
40 | `Time: ${time} -- Parameters: ${this.stringifyParameters(
41 | parameters,
42 | )} -- ${query}`,
43 | );
44 | }
45 | logMigration(message: string) {
46 | this.logger.log(message);
47 | }
48 | logSchemaBuild(message: string) {
49 | this.logger.log(message);
50 | }
51 | log(
52 | level: 'log' | 'info' | 'warn',
53 | message: string,
54 | queryRunner?: QueryRunner,
55 | ) {
56 | if (queryRunner?.data?.isCreatingLogs) {
57 | return;
58 | }
59 | if (level === 'log') {
60 | return this.logger.log(message);
61 | }
62 | if (level === 'info') {
63 | return this.logger.debug(message);
64 | }
65 | if (level === 'warn') {
66 | return this.logger.warn(message);
67 | }
68 | }
69 | private stringifyParameters(parameters?: unknown[]) {
70 | try {
71 | return JSON.stringify(parameters);
72 | } catch {
73 | return '';
74 | }
75 | }
76 | }
77 |
78 | export default DatabaseLogger;
79 |
--------------------------------------------------------------------------------
/src/database/postgresErrorCode.enum.ts:
--------------------------------------------------------------------------------
1 | enum PostgresErrorCode {
2 | UniqueViolation = '23505',
3 | }
4 |
5 | export default PostgresErrorCode;
6 |
--------------------------------------------------------------------------------
/src/databaseFiles/databaseFile.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2 |
3 | @Entity()
4 | class DatabaseFile {
5 | @PrimaryGeneratedColumn()
6 | public id: number;
7 |
8 | @Column()
9 | filename: string;
10 |
11 | @Column({
12 | type: 'bytea',
13 | })
14 | data: Uint8Array;
15 | }
16 |
17 | export default DatabaseFile;
18 |
--------------------------------------------------------------------------------
/src/databaseFiles/databaseFiles.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { ConfigModule } from '@nestjs/config';
4 | import DatabaseFile from './databaseFile.entity';
5 | import DatabaseFilesService from './databaseFiles.services';
6 | import DatabaseFilesController from './databaseFilesController';
7 |
8 | @Module({
9 | imports: [TypeOrmModule.forFeature([DatabaseFile]), ConfigModule],
10 | providers: [DatabaseFilesService],
11 | exports: [DatabaseFilesService],
12 | controllers: [DatabaseFilesController],
13 | })
14 | export class DatabaseFilesModule {}
15 |
--------------------------------------------------------------------------------
/src/databaseFiles/databaseFiles.services.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { QueryRunner, Repository } from 'typeorm';
4 | import DatabaseFile from './databaseFile.entity';
5 |
6 | @Injectable()
7 | class DatabaseFilesService {
8 | constructor(
9 | @InjectRepository(DatabaseFile)
10 | private databaseFilesRepository: Repository,
11 | ) {}
12 |
13 | async uploadDatabaseFileWithQueryRunner(
14 | dataBuffer: Buffer,
15 | filename: string,
16 | queryRunner: QueryRunner,
17 | ) {
18 | const newFile = await queryRunner.manager.create(DatabaseFile, {
19 | filename,
20 | data: dataBuffer,
21 | });
22 | await queryRunner.manager.save(DatabaseFile, newFile);
23 | return newFile;
24 | }
25 |
26 | async deleteFileWithQueryRunner(fileId: number, queryRunner: QueryRunner) {
27 | const deleteResponse = await queryRunner.manager.delete(
28 | DatabaseFile,
29 | fileId,
30 | );
31 | if (!deleteResponse.affected) {
32 | throw new NotFoundException();
33 | }
34 | }
35 |
36 | async getFileById(fileId: number) {
37 | const file = await this.databaseFilesRepository.findOneBy({
38 | id: fileId,
39 | });
40 | if (!file) {
41 | throw new NotFoundException();
42 | }
43 | return file;
44 | }
45 | }
46 |
47 | export default DatabaseFilesService;
48 |
--------------------------------------------------------------------------------
/src/databaseFiles/databaseFilesController.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Param,
5 | UseInterceptors,
6 | ClassSerializerInterceptor,
7 | StreamableFile,
8 | Res,
9 | ParseIntPipe,
10 | } from '@nestjs/common';
11 | import DatabaseFilesService from './databaseFiles.services';
12 | import { Readable } from 'stream';
13 | import { Response } from 'express';
14 |
15 | @Controller('database-files')
16 | @UseInterceptors(ClassSerializerInterceptor)
17 | export default class DatabaseFilesController {
18 | constructor(private readonly databaseFilesService: DatabaseFilesService) {}
19 |
20 | @Get(':id')
21 | async getDatabaseFileById(
22 | @Param('id', ParseIntPipe) id: number,
23 | @Res({ passthrough: true }) response: Response,
24 | ) {
25 | const file = await this.databaseFilesService.getFileById(id);
26 |
27 | const stream = Readable.from(file.data);
28 |
29 | response.set({
30 | 'Content-Disposition': `inline; filename="${file.filename}"`,
31 | 'Content-Type': 'image',
32 | });
33 |
34 | return new StreamableFile(stream);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/email/email.module-definition.ts:
--------------------------------------------------------------------------------
1 | import { ConfigurableModuleBuilder } from '@nestjs/common';
2 | import EmailOptions from './emailOptions.interface';
3 |
4 | export const {
5 | ConfigurableModuleClass: ConfigurableEmailModule,
6 | MODULE_OPTIONS_TOKEN: EMAIL_CONFIG_OPTIONS,
7 | } = new ConfigurableModuleBuilder().build();
8 |
--------------------------------------------------------------------------------
/src/email/email.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigurableEmailModule } from './email.module-definition';
3 | import EmailService from './email.service';
4 |
5 | @Module({
6 | providers: [EmailService],
7 | exports: [EmailService],
8 | })
9 | export class EmailModule extends ConfigurableEmailModule {}
10 |
--------------------------------------------------------------------------------
/src/email/email.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@nestjs/common';
2 | import { createTransport } from 'nodemailer';
3 | import Mail from 'nodemailer/lib/mailer';
4 | import EmailOptions from './emailOptions.interface';
5 | import { EMAIL_CONFIG_OPTIONS } from './email.module-definition';
6 |
7 | @Injectable()
8 | export default class EmailService {
9 | private nodemailerTransport: Mail;
10 |
11 | constructor(@Inject(EMAIL_CONFIG_OPTIONS) private options: EmailOptions) {
12 | this.nodemailerTransport = createTransport({
13 | service: options.service,
14 | auth: {
15 | user: options.user,
16 | pass: options.password,
17 | },
18 | });
19 | }
20 |
21 | sendMail(options: Mail.Options) {
22 | return this.nodemailerTransport.sendMail(options);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/email/emailAsyncOptions.type.ts:
--------------------------------------------------------------------------------
1 | import { ModuleMetadata } from '@nestjs/common';
2 | import EmailOptions from './emailOptions.interface';
3 | import { FactoryProvider } from '@nestjs/common/interfaces/modules/provider.interface';
4 |
5 | type EmailAsyncOptions = Pick &
6 | Pick, 'useFactory' | 'inject'>;
7 |
8 | export default EmailAsyncOptions;
9 |
--------------------------------------------------------------------------------
/src/email/emailOptions.interface.ts:
--------------------------------------------------------------------------------
1 | interface EmailOptions {
2 | service: string;
3 | user: string;
4 | password: string;
5 | }
6 |
7 | export default EmailOptions;
8 |
--------------------------------------------------------------------------------
/src/emailConfirmation/confirmEmail.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty } from 'class-validator';
2 |
3 | export class ConfirmEmailDto {
4 | @IsString()
5 | @IsNotEmpty()
6 | token: string;
7 | }
8 |
9 | export default ConfirmEmailDto;
10 |
--------------------------------------------------------------------------------
/src/emailConfirmation/emailConfirmation.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | ClassSerializerInterceptor,
4 | UseInterceptors,
5 | Post,
6 | Body,
7 | UseGuards,
8 | Req,
9 | } from '@nestjs/common';
10 | import ConfirmEmailDto from './confirmEmail.dto';
11 | import { EmailConfirmationService } from './emailConfirmation.service';
12 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
13 | import RequestWithUser from '../authentication/requestWithUser.interface';
14 |
15 | @Controller('email-confirmation')
16 | @UseInterceptors(ClassSerializerInterceptor)
17 | export class EmailConfirmationController {
18 | constructor(
19 | private readonly emailConfirmationService: EmailConfirmationService,
20 | ) {}
21 |
22 | @Post('confirm')
23 | async confirm(@Body() confirmationData: ConfirmEmailDto) {
24 | const email = await this.emailConfirmationService.decodeConfirmationToken(
25 | confirmationData.token,
26 | );
27 | await this.emailConfirmationService.confirmEmail(email);
28 | }
29 |
30 | @Post('resend-confirmation-link')
31 | @UseGuards(JwtAuthenticationGuard)
32 | async resendConfirmationLink(@Req() request: RequestWithUser) {
33 | await this.emailConfirmationService.resendConfirmationLink(request.user.id);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/emailConfirmation/emailConfirmation.guard.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | CanActivate,
4 | ExecutionContext,
5 | UnauthorizedException,
6 | } from '@nestjs/common';
7 | import RequestWithUser from '../authentication/requestWithUser.interface';
8 |
9 | @Injectable()
10 | export class EmailConfirmationGuard implements CanActivate {
11 | canActivate(context: ExecutionContext) {
12 | const request: RequestWithUser = context.switchToHttp().getRequest();
13 |
14 | if (!request.user?.isEmailConfirmed) {
15 | throw new UnauthorizedException('Confirm your email first');
16 | }
17 |
18 | return true;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/emailConfirmation/emailConfirmation.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { EmailConfirmationService } from './emailConfirmation.service';
3 | import { ConfigModule, ConfigService } from '@nestjs/config';
4 | import { EmailModule } from '../email/email.module';
5 | import { JwtModule } from '@nestjs/jwt';
6 | import { EmailConfirmationController } from './emailConfirmation.controller';
7 | import { UsersModule } from '../users/users.module';
8 |
9 | @Module({
10 | imports: [
11 | ConfigModule,
12 | EmailModule.registerAsync({
13 | imports: [ConfigModule],
14 | inject: [ConfigService],
15 | useFactory: (configService: ConfigService) => ({
16 | service: configService.get('EMAIL_SERVICE'),
17 | user: configService.get('EMAIL_USER'),
18 | password: configService.get('EMAIL_PASSWORD'),
19 | }),
20 | }),
21 | JwtModule.register({}),
22 | UsersModule,
23 | ],
24 | providers: [EmailConfirmationService],
25 | exports: [EmailConfirmationService],
26 | controllers: [EmailConfirmationController],
27 | })
28 | export class EmailConfirmationModule {}
29 |
--------------------------------------------------------------------------------
/src/emailConfirmation/emailConfirmation.service.ts:
--------------------------------------------------------------------------------
1 | import { BadRequestException, Injectable } from '@nestjs/common';
2 | import { JwtService } from '@nestjs/jwt';
3 | import { ConfigService } from '@nestjs/config';
4 | import VerificationTokenPayload from './verificationTokenPayload.interface';
5 | import EmailService from '../email/email.service';
6 | import { UsersService } from '../users/users.service';
7 |
8 | @Injectable()
9 | export class EmailConfirmationService {
10 | constructor(
11 | private readonly jwtService: JwtService,
12 | private readonly configService: ConfigService,
13 | private readonly emailService: EmailService,
14 | private readonly usersService: UsersService,
15 | ) {}
16 |
17 | public sendVerificationLink(email: string) {
18 | const payload: VerificationTokenPayload = { email };
19 | const token = this.jwtService.sign(payload, {
20 | secret: this.configService.get('JWT_VERIFICATION_TOKEN_SECRET'),
21 | expiresIn: `${this.configService.get(
22 | 'JWT_VERIFICATION_TOKEN_EXPIRATION_TIME',
23 | )}s`,
24 | });
25 |
26 | const url = `${this.configService.get(
27 | 'EMAIL_CONFIRMATION_URL',
28 | )}?token=${token}`;
29 |
30 | const text = `Welcome to the application. To confirm the email address, click here: ${url}`;
31 |
32 | return this.emailService.sendMail({
33 | to: email,
34 | subject: 'Email confirmation',
35 | text,
36 | });
37 | }
38 |
39 | public async resendConfirmationLink(userId: number) {
40 | const user = await this.usersService.getById(userId);
41 | if (user.isEmailConfirmed) {
42 | throw new BadRequestException('Email already confirmed');
43 | }
44 | await this.sendVerificationLink(user.email);
45 | }
46 |
47 | public async confirmEmail(email: string) {
48 | const user = await this.usersService.getByEmail(email);
49 | if (user.isEmailConfirmed) {
50 | throw new BadRequestException('Email already confirmed');
51 | }
52 | await this.usersService.markEmailAsConfirmed(email);
53 | }
54 |
55 | public async decodeConfirmationToken(token: string) {
56 | try {
57 | const payload = await this.jwtService.verify(token, {
58 | secret: this.configService.get('JWT_VERIFICATION_TOKEN_SECRET'),
59 | });
60 |
61 | if (typeof payload === 'object' && 'email' in payload) {
62 | return payload.email;
63 | }
64 | throw new BadRequestException();
65 | } catch (error) {
66 | if (error?.name === 'TokenExpiredError') {
67 | throw new BadRequestException('Email confirmation token expired');
68 | }
69 | throw new BadRequestException('Bad confirmation token');
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/emailConfirmation/verificationTokenPayload.interface.ts:
--------------------------------------------------------------------------------
1 | interface VerificationTokenPayload {
2 | email: string;
3 | }
4 |
5 | export default VerificationTokenPayload;
6 |
--------------------------------------------------------------------------------
/src/emailScheduling/dto/emailSchedule.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty, IsDateString, IsEmail } from 'class-validator';
2 |
3 | export class EmailScheduleDto {
4 | @IsEmail()
5 | recipient: string;
6 |
7 | @IsString()
8 | @IsNotEmpty()
9 | subject: string;
10 |
11 | @IsString()
12 | @IsNotEmpty()
13 | content: string;
14 |
15 | @IsDateString()
16 | date: string;
17 | }
18 |
19 | export default EmailScheduleDto;
20 |
--------------------------------------------------------------------------------
/src/emailScheduling/emailScheduling.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, UseGuards, Post } from '@nestjs/common';
2 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
3 | import EmailSchedulingService from './emailScheduling.service';
4 | import EmailScheduleDto from './dto/emailSchedule.dto';
5 |
6 | @Controller('email-scheduling')
7 | export default class EmailSchedulingController {
8 | constructor(
9 | private readonly emailSchedulingService: EmailSchedulingService,
10 | ) {}
11 |
12 | @Post('schedule')
13 | @UseGuards(JwtAuthenticationGuard)
14 | async scheduleEmail(@Body() emailSchedule: EmailScheduleDto) {
15 | this.emailSchedulingService.scheduleEmail(emailSchedule);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/emailScheduling/emailScheduling.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import EmailSchedulingService from './emailScheduling.service';
3 | import { EmailModule } from '../email/email.module';
4 | import EmailSchedulingController from './emailScheduling.controller';
5 | import { ConfigModule, ConfigService } from '@nestjs/config';
6 |
7 | @Module({
8 | imports: [
9 | EmailModule.registerAsync({
10 | imports: [ConfigModule],
11 | inject: [ConfigService],
12 | useFactory: (configService: ConfigService) => ({
13 | service: configService.get('EMAIL_SERVICE'),
14 | user: configService.get('EMAIL_USER'),
15 | password: configService.get('EMAIL_PASSWORD'),
16 | }),
17 | }),
18 | ],
19 | controllers: [EmailSchedulingController],
20 | providers: [EmailSchedulingService],
21 | })
22 | export class EmailSchedulingModule {}
23 |
--------------------------------------------------------------------------------
/src/emailScheduling/emailScheduling.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import EmailService from '../email/email.service';
3 | import EmailScheduleDto from './dto/emailSchedule.dto';
4 | import { SchedulerRegistry } from '@nestjs/schedule';
5 | import { CronJob } from 'cron';
6 |
7 | @Injectable()
8 | export default class EmailSchedulingService {
9 | constructor(
10 | private readonly emailService: EmailService,
11 | private readonly schedulerRegistry: SchedulerRegistry,
12 | ) {}
13 |
14 | scheduleEmail(emailSchedule: EmailScheduleDto) {
15 | const date = new Date(emailSchedule.date);
16 | const job = new CronJob(date, () => {
17 | this.emailService.sendMail({
18 | to: emailSchedule.recipient,
19 | subject: emailSchedule.subject,
20 | text: emailSchedule.content,
21 | });
22 | });
23 |
24 | this.schedulerRegistry.addCronJob(
25 | `${Date.now()}-${emailSchedule.subject}`,
26 | job,
27 | );
28 | job.start();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/files/files.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { FilesService } from './files.service';
4 | import { ConfigModule } from '@nestjs/config';
5 | import PublicFile from './publicFile.entity';
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([PublicFile]), ConfigModule],
9 | providers: [FilesService],
10 | exports: [FilesService],
11 | })
12 | export class FilesModule {}
13 |
--------------------------------------------------------------------------------
/src/files/files.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository, QueryRunner } from 'typeorm';
4 | import PublicFile from './publicFile.entity';
5 | import { S3 } from 'aws-sdk';
6 | import { ConfigService } from '@nestjs/config';
7 | import { v4 as uuid } from 'uuid';
8 |
9 | @Injectable()
10 | export class FilesService {
11 | constructor(
12 | @InjectRepository(PublicFile)
13 | private publicFilesRepository: Repository,
14 | private readonly configService: ConfigService,
15 | ) {}
16 |
17 | async uploadPublicFile(dataBuffer: Buffer, filename: string) {
18 | const s3 = new S3();
19 | const uploadResult = await s3
20 | .upload({
21 | Bucket: this.configService.get('AWS_PUBLIC_BUCKET_NAME'),
22 | Body: dataBuffer,
23 | Key: `${uuid()}-${filename}`,
24 | })
25 | .promise();
26 |
27 | const newFile = this.publicFilesRepository.create({
28 | key: uploadResult.Key,
29 | url: uploadResult.Location,
30 | });
31 | await this.publicFilesRepository.save(newFile);
32 | return newFile;
33 | }
34 |
35 | async deletePublicFile(fileId: number) {
36 | const file = await this.publicFilesRepository.findOneBy({ id: fileId });
37 | const s3 = new S3();
38 | await s3
39 | .deleteObject({
40 | Bucket: this.configService.get('AWS_PUBLIC_BUCKET_NAME'),
41 | Key: file.key,
42 | })
43 | .promise();
44 | await this.publicFilesRepository.delete(fileId);
45 | }
46 |
47 | async deletePublicFileWithQueryRunner(
48 | fileId: number,
49 | queryRunner: QueryRunner,
50 | ) {
51 | const file = await queryRunner.manager.findOneBy(PublicFile, {
52 | id: fileId,
53 | });
54 | const s3 = new S3();
55 | await s3
56 | .deleteObject({
57 | Bucket: this.configService.get('AWS_PUBLIC_BUCKET_NAME'),
58 | Key: file.key,
59 | })
60 | .promise();
61 | await queryRunner.manager.delete(PublicFile, fileId);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/files/publicFile.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2 |
3 | @Entity()
4 | class PublicFile {
5 | @PrimaryGeneratedColumn()
6 | public id: number;
7 |
8 | @Column()
9 | public url: string;
10 |
11 | @Column()
12 | public key: string;
13 | }
14 |
15 | export default PublicFile;
16 |
--------------------------------------------------------------------------------
/src/googleAuthentication/googleAuthentication.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Post,
4 | ClassSerializerInterceptor,
5 | UseInterceptors,
6 | Body,
7 | Req,
8 | } from '@nestjs/common';
9 | import TokenVerificationDto from './tokenVerification.dto';
10 | import { GoogleAuthenticationService } from './googleAuthentication.service';
11 | import { Request } from 'express';
12 |
13 | @Controller('google-authentication')
14 | @UseInterceptors(ClassSerializerInterceptor)
15 | export class GoogleAuthenticationController {
16 | constructor(
17 | private readonly googleAuthenticationService: GoogleAuthenticationService,
18 | ) {}
19 |
20 | @Post()
21 | async authenticate(
22 | @Body() tokenData: TokenVerificationDto,
23 | @Req() request: Request,
24 | ) {
25 | const {
26 | accessTokenCookie,
27 | refreshTokenCookie,
28 | user,
29 | } = await this.googleAuthenticationService.authenticate(tokenData.token);
30 |
31 | request.res.setHeader('Set-Cookie', [
32 | accessTokenCookie,
33 | refreshTokenCookie,
34 | ]);
35 |
36 | return user;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/googleAuthentication/googleAuthentication.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { GoogleAuthenticationController } from './googleAuthentication.controller';
3 | import { ConfigModule } from '@nestjs/config';
4 | import { UsersModule } from '../users/users.module';
5 | import { GoogleAuthenticationService } from './googleAuthentication.service';
6 | import { AuthenticationModule } from '../authentication/authentication.module';
7 |
8 | @Module({
9 | imports: [ConfigModule, UsersModule, AuthenticationModule],
10 | providers: [GoogleAuthenticationService],
11 | controllers: [GoogleAuthenticationController],
12 | exports: [],
13 | })
14 | export class GoogleAuthenticationModule {}
15 |
--------------------------------------------------------------------------------
/src/googleAuthentication/googleAuthentication.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, UnauthorizedException } from '@nestjs/common';
2 | import { UsersService } from '../users/users.service';
3 | import { ConfigService } from '@nestjs/config';
4 | import { google, Auth } from 'googleapis';
5 | import { AuthenticationService } from '../authentication/authentication.service';
6 | import User from '../users/user.entity';
7 |
8 | @Injectable()
9 | export class GoogleAuthenticationService {
10 | oauthClient: Auth.OAuth2Client;
11 | constructor(
12 | private readonly usersService: UsersService,
13 | private readonly configService: ConfigService,
14 | private readonly authenticationService: AuthenticationService,
15 | ) {
16 | const clientID = this.configService.get('GOOGLE_AUTH_CLIENT_ID');
17 | const clientSecret = this.configService.get('GOOGLE_AUTH_CLIENT_SECRET');
18 |
19 | this.oauthClient = new google.auth.OAuth2(clientID, clientSecret);
20 | }
21 |
22 | async getUserData(token: string) {
23 | const userInfoClient = google.oauth2('v2').userinfo;
24 |
25 | this.oauthClient.setCredentials({
26 | access_token: token,
27 | });
28 |
29 | const userInfoResponse = await userInfoClient.get({
30 | auth: this.oauthClient,
31 | });
32 |
33 | return userInfoResponse.data;
34 | }
35 |
36 | async getCookiesForUser(user: User) {
37 | const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken(
38 | user.id,
39 | );
40 | const {
41 | cookie: refreshTokenCookie,
42 | token: refreshToken,
43 | } = this.authenticationService.getCookieWithJwtRefreshToken(user.id);
44 |
45 | await this.usersService.setCurrentRefreshToken(refreshToken, user.id);
46 |
47 | return {
48 | accessTokenCookie,
49 | refreshTokenCookie,
50 | };
51 | }
52 |
53 | async handleRegisteredUser(user: User) {
54 | if (!user.isRegisteredWithGoogle) {
55 | throw new UnauthorizedException();
56 | }
57 |
58 | const {
59 | accessTokenCookie,
60 | refreshTokenCookie,
61 | } = await this.getCookiesForUser(user);
62 |
63 | return {
64 | accessTokenCookie,
65 | refreshTokenCookie,
66 | user,
67 | };
68 | }
69 |
70 | async registerUser(token: string, email: string) {
71 | const userData = await this.getUserData(token);
72 | const name = userData.name;
73 |
74 | const user = await this.usersService.createWithGoogle(email, name);
75 |
76 | return this.handleRegisteredUser(user);
77 | }
78 |
79 | async authenticate(token: string) {
80 | const tokenInfo = await this.oauthClient.getTokenInfo(token);
81 |
82 | const email = tokenInfo.email;
83 |
84 | try {
85 | const user = await this.usersService.getByEmail(email);
86 |
87 | return this.handleRegisteredUser(user);
88 | } catch (error) {
89 | if (error.status !== 404) {
90 | throw new error();
91 | }
92 |
93 | return this.registerUser(token, email);
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/googleAuthentication/tokenVerification.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty } from 'class-validator';
2 |
3 | export class TokenVerificationDto {
4 | @IsString()
5 | @IsNotEmpty()
6 | token: string;
7 | }
8 |
9 | export default TokenVerificationDto;
10 |
--------------------------------------------------------------------------------
/src/health/elasticsearchHealthIndicator.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import {
3 | HealthIndicator,
4 | HealthIndicatorResult,
5 | HealthCheckError,
6 | } from '@nestjs/terminus';
7 | import { ElasticsearchService } from '@nestjs/elasticsearch';
8 |
9 | @Injectable()
10 | export class ElasticsearchHealthIndicator extends HealthIndicator {
11 | constructor(private readonly elasticsearchService: ElasticsearchService) {
12 | super();
13 | }
14 |
15 | async isHealthy(key: string): Promise {
16 | try {
17 | await this.elasticsearchService.ping();
18 | return this.getStatus(key, true);
19 | } catch (error) {
20 | throw new HealthCheckError(
21 | 'ElasticsearchHealthIndicator failed',
22 | this.getStatus(key, false),
23 | );
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/health/health.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 | import {
3 | HealthCheckService,
4 | HealthCheck,
5 | TypeOrmHealthIndicator,
6 | MemoryHealthIndicator,
7 | DiskHealthIndicator,
8 | } from '@nestjs/terminus';
9 | import { ElasticsearchHealthIndicator } from './elasticsearchHealthIndicator';
10 |
11 | @Controller('health')
12 | class HealthController {
13 | constructor(
14 | private healthCheckService: HealthCheckService,
15 | private typeOrmHealthIndicator: TypeOrmHealthIndicator,
16 | private memoryHealthIndicator: MemoryHealthIndicator,
17 | private diskHealthIndicator: DiskHealthIndicator,
18 | private elasticsearchHealthIndicator: ElasticsearchHealthIndicator,
19 | ) {}
20 |
21 | @Get()
22 | @HealthCheck()
23 | check() {
24 | return this.healthCheckService.check([
25 | () => this.typeOrmHealthIndicator.pingCheck('database'),
26 | // the process should not use more than 300MB memory
27 | () =>
28 | this.memoryHealthIndicator.checkHeap('memory heap', 300 * 1024 * 1024),
29 | // The process should not have more than 300MB RSS memory allocated
30 | () =>
31 | this.memoryHealthIndicator.checkRSS('memory RSS', 300 * 1024 * 1024),
32 | // the used disk storage should not exceed the 50% of the available space
33 | () =>
34 | this.diskHealthIndicator.checkStorage('disk health', {
35 | thresholdPercent: 0.5,
36 | path: '/',
37 | }),
38 | () => this.elasticsearchHealthIndicator.isHealthy('elasticsearch'),
39 | ]);
40 | }
41 | }
42 |
43 | export default HealthController;
44 |
--------------------------------------------------------------------------------
/src/health/health.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import HealthController from './health.controller';
3 | import { TerminusModule } from '@nestjs/terminus';
4 | import { ElasticsearchHealthIndicator } from './elasticsearchHealthIndicator';
5 | import { SearchModule } from '../search/search.module';
6 |
7 | @Module({
8 | imports: [TerminusModule, SearchModule],
9 | controllers: [HealthController],
10 | providers: [ElasticsearchHealthIndicator],
11 | })
12 | export default class HealthModule {}
13 |
--------------------------------------------------------------------------------
/src/localFiles/localFile.dto.ts:
--------------------------------------------------------------------------------
1 | interface LocalFileDto {
2 | filename: string;
3 | path: string;
4 | mimetype: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/localFiles/localFile.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2 |
3 | @Entity()
4 | class LocalFile {
5 | @PrimaryGeneratedColumn()
6 | public id: number;
7 |
8 | @Column()
9 | filename: string;
10 |
11 | @Column()
12 | path: string;
13 |
14 | @Column()
15 | mimetype: string;
16 | }
17 |
18 | export default LocalFile;
19 |
--------------------------------------------------------------------------------
/src/localFiles/localFiles.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Param,
5 | UseInterceptors,
6 | ClassSerializerInterceptor,
7 | StreamableFile,
8 | Res,
9 | ParseIntPipe,
10 | } from '@nestjs/common';
11 | import LocalFilesService from './localFiles.service';
12 | import { Response } from 'express';
13 | import { createReadStream } from 'fs';
14 | import { join } from 'path';
15 |
16 | @Controller('local-files')
17 | @UseInterceptors(ClassSerializerInterceptor)
18 | export default class LocalFilesController {
19 | constructor(private readonly localFilesService: LocalFilesService) {}
20 |
21 | @Get(':id')
22 | async getDatabaseFileById(
23 | @Param('id', ParseIntPipe) id: number,
24 | @Res({ passthrough: true }) response: Response,
25 | ) {
26 | const file = await this.localFilesService.getFileById(id);
27 |
28 | const stream = createReadStream(join(process.cwd(), file.path));
29 |
30 | response.set({
31 | 'Content-Disposition': `inline; filename="${file.filename}"`,
32 | 'Content-Type': file.mimetype,
33 | });
34 | return new StreamableFile(stream);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/localFiles/localFiles.interceptor.ts:
--------------------------------------------------------------------------------
1 | import { FileInterceptor } from '@nestjs/platform-express';
2 | import { Injectable, mixin, NestInterceptor, Type } from '@nestjs/common';
3 | import { ConfigService } from '@nestjs/config';
4 | import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
5 | import { diskStorage } from 'multer';
6 |
7 | interface LocalFilesInterceptorOptions {
8 | fieldName: string;
9 | path?: string;
10 | fileFilter?: MulterOptions['fileFilter'];
11 | limits?: MulterOptions['limits'];
12 | }
13 |
14 | function LocalFilesInterceptor(
15 | options: LocalFilesInterceptorOptions,
16 | ): Type {
17 | @Injectable()
18 | class Interceptor implements NestInterceptor {
19 | fileInterceptor: NestInterceptor;
20 | constructor(configService: ConfigService) {
21 | const filesDestination = configService.get('UPLOADED_FILES_DESTINATION');
22 |
23 | const destination = `${filesDestination}${options.path}`;
24 |
25 | const multerOptions: MulterOptions = {
26 | storage: diskStorage({
27 | destination,
28 | }),
29 | fileFilter: options.fileFilter,
30 | limits: options.limits,
31 | };
32 |
33 | this.fileInterceptor = new (FileInterceptor(
34 | options.fieldName,
35 | multerOptions,
36 | ))();
37 | }
38 |
39 | intercept(...args: Parameters) {
40 | return this.fileInterceptor.intercept(...args);
41 | }
42 | }
43 | return mixin(Interceptor);
44 | }
45 |
46 | export default LocalFilesInterceptor;
47 |
--------------------------------------------------------------------------------
/src/localFiles/localFiles.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { ConfigModule } from '@nestjs/config';
4 | import LocalFile from './localFile.entity';
5 | import LocalFilesService from './localFiles.service';
6 | import LocalFilesController from './localFiles.controller';
7 |
8 | @Module({
9 | imports: [TypeOrmModule.forFeature([LocalFile]), ConfigModule],
10 | providers: [LocalFilesService],
11 | exports: [LocalFilesService],
12 | controllers: [LocalFilesController],
13 | })
14 | export class LocalFilesModule {}
15 |
--------------------------------------------------------------------------------
/src/localFiles/localFiles.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NotFoundException } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository } from 'typeorm';
4 | import LocalFile from './localFile.entity';
5 |
6 | @Injectable()
7 | class LocalFilesService {
8 | constructor(
9 | @InjectRepository(LocalFile)
10 | private localFilesRepository: Repository,
11 | ) {}
12 |
13 | async saveLocalFileData(fileData: LocalFileDto) {
14 | const newFile = await this.localFilesRepository.create(fileData);
15 | await this.localFilesRepository.save(newFile);
16 | return newFile;
17 | }
18 |
19 | async getFileById(fileId: number) {
20 | const file = await this.localFilesRepository.findOneBy({ id: fileId });
21 | if (!file) {
22 | throw new NotFoundException();
23 | }
24 | return file;
25 | }
26 | }
27 |
28 | export default LocalFilesService;
29 |
--------------------------------------------------------------------------------
/src/logger/customLogger.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, ConsoleLogger } from '@nestjs/common';
2 | import { ConsoleLoggerOptions } from '@nestjs/common/services/console-logger.service';
3 | import { ConfigService } from '@nestjs/config';
4 | import getLogLevels from '../utils/getLogLevels';
5 | import LogsService from './logs.service';
6 |
7 | @Injectable()
8 | class CustomLogger extends ConsoleLogger {
9 | private readonly logsService: LogsService;
10 |
11 | constructor(
12 | context: string,
13 | options: ConsoleLoggerOptions,
14 | configService: ConfigService,
15 | logsService: LogsService,
16 | ) {
17 | const environment = configService.get('NODE_ENV');
18 |
19 | super(context, {
20 | ...options,
21 | logLevels: getLogLevels(environment === 'production'),
22 | });
23 |
24 | this.logsService = logsService;
25 | }
26 |
27 | log(message: string, context?: string) {
28 | super.log.apply(this, [message, context]);
29 |
30 | this.logsService.createLog({
31 | message,
32 | context,
33 | level: 'log',
34 | });
35 | }
36 | error(message: string, context?: string, stack?: string) {
37 | super.error.apply(this, [message, context, stack]);
38 |
39 | this.logsService.createLog({
40 | message,
41 | context,
42 | level: 'error',
43 | });
44 | }
45 | warn(message: string, context?: string) {
46 | super.warn.apply(this, [message, context]);
47 |
48 | this.logsService.createLog({
49 | message,
50 | context,
51 | level: 'error',
52 | });
53 | }
54 | debug(message: string, context?: string) {
55 | super.debug.apply(this, [message, context]);
56 |
57 | this.logsService.createLog({
58 | message,
59 | context,
60 | level: 'error',
61 | });
62 | }
63 | verbose(message: string, context?: string) {
64 | super.debug.apply(this, [message, context]);
65 |
66 | this.logsService.createLog({
67 | message,
68 | context,
69 | level: 'error',
70 | });
71 | }
72 | }
73 |
74 | export default CustomLogger;
75 |
--------------------------------------------------------------------------------
/src/logger/dto/createLog.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateLogDto {
2 | context: string;
3 | message: string;
4 | level: string;
5 | }
6 |
7 | export default CreateLogDto;
8 |
--------------------------------------------------------------------------------
/src/logger/log.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | CreateDateColumn,
4 | Entity,
5 | PrimaryGeneratedColumn,
6 | } from 'typeorm';
7 |
8 | @Entity()
9 | class Log {
10 | @PrimaryGeneratedColumn()
11 | public id: number;
12 |
13 | @Column()
14 | public context: string;
15 |
16 | @Column()
17 | public message: string;
18 |
19 | @Column()
20 | public level: string;
21 |
22 | @CreateDateColumn()
23 | creationDate: Date;
24 | }
25 |
26 | export default Log;
27 |
--------------------------------------------------------------------------------
/src/logger/logger.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import CustomLogger from './customLogger';
3 | import { ConfigModule } from '@nestjs/config';
4 | import LogsService from './logs.service';
5 | import { TypeOrmModule } from '@nestjs/typeorm';
6 | import Log from './log.entity';
7 |
8 | @Module({
9 | imports: [ConfigModule, TypeOrmModule.forFeature([Log])],
10 | providers: [CustomLogger, LogsService],
11 | exports: [CustomLogger],
12 | })
13 | export class LoggerModule {}
14 |
--------------------------------------------------------------------------------
/src/logger/logs.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository } from 'typeorm';
4 | import Log from './log.entity';
5 | import CreateLogDto from './dto/createLog.dto';
6 |
7 | @Injectable()
8 | export default class LogsService {
9 | constructor(
10 | @InjectRepository(Log)
11 | private logsRepository: Repository,
12 | ) {}
13 |
14 | async createLog(log: CreateLogDto) {
15 | const newLog = await this.logsRepository.create(log);
16 | await this.logsRepository.save(newLog, {
17 | data: {
18 | isCreatingLogs: true,
19 | },
20 | });
21 | return newLog;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import * as cookieParser from 'cookie-parser';
4 | import { ValidationPipe } from '@nestjs/common';
5 | import { ConfigService } from '@nestjs/config';
6 | import { config } from 'aws-sdk';
7 | import rawBodyMiddleware from './utils/rawBody.middleware';
8 | import CustomLogger from './logger/customLogger';
9 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
10 |
11 | async function bootstrap() {
12 | const app = await NestFactory.create(AppModule, {
13 | bufferLogs: true,
14 | });
15 | app.useLogger(app.get(CustomLogger));
16 | app.useGlobalPipes(
17 | new ValidationPipe({
18 | transform: true,
19 | }),
20 | );
21 | app.use(cookieParser());
22 |
23 | const configService = app.get(ConfigService);
24 | config.update({
25 | accessKeyId: configService.get('AWS_ACCESS_KEY_ID'),
26 | secretAccessKey: configService.get('AWS_SECRET_ACCESS_KEY'),
27 | region: configService.get('AWS_REGION'),
28 | });
29 |
30 | app.enableCors({
31 | origin: configService.get('FRONTEND_URL'),
32 | credentials: true,
33 | });
34 |
35 | app.use(rawBodyMiddleware());
36 |
37 | const swaggerConfig = new DocumentBuilder()
38 | .setTitle('API with NestJS')
39 | .setDescription('API developed throughout the API with NestJS course')
40 | .setVersion('1.0')
41 | .build();
42 |
43 | const document = SwaggerModule.createDocument(app, swaggerConfig);
44 | SwaggerModule.setup('api', app, document);
45 |
46 | const port = configService.get('PORT') ?? 3000;
47 |
48 | await app.listen(port);
49 | }
50 | bootstrap();
51 |
--------------------------------------------------------------------------------
/src/optimize/image.processor.ts:
--------------------------------------------------------------------------------
1 | import * as AdmZip from 'adm-zip';
2 | import { buffer } from 'imagemin';
3 | import imageminPngquant from 'imagemin-pngquant';
4 | import { Express } from 'express';
5 | import { Job, DoneCallback } from 'bull';
6 |
7 | async function imageProcessor(job: Job, doneCallback: DoneCallback) {
8 | const files: Express.Multer.File[] = job.data.files;
9 |
10 | const optimizationPromises: Promise[] = files.map(file => {
11 | const fileBuffer = Buffer.from(file.buffer);
12 | return buffer(fileBuffer, {
13 | plugins: [
14 | imageminPngquant({
15 | quality: [0.6, 0.8],
16 | }),
17 | ],
18 | });
19 | });
20 |
21 | const optimizedImages = await Promise.all(optimizationPromises);
22 |
23 | const zip = new AdmZip();
24 |
25 | optimizedImages.forEach((image, index) => {
26 | const fileData = files[index];
27 | zip.addFile(fileData.originalname, image);
28 | });
29 |
30 | doneCallback(null, zip.toBuffer());
31 | }
32 |
33 | export default imageProcessor;
34 |
--------------------------------------------------------------------------------
/src/optimize/optimize.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Param,
5 | Post,
6 | Res,
7 | UploadedFiles,
8 | UseInterceptors,
9 | } from '@nestjs/common';
10 | import { Response } from 'express';
11 | import { InjectQueue } from '@nestjs/bull';
12 | import { Queue } from 'bull';
13 | import { Readable } from 'stream';
14 | import { AnyFilesInterceptor } from '@nestjs/platform-express';
15 |
16 | @Controller('optimize')
17 | export class OptimizeController {
18 | constructor(@InjectQueue('image') private readonly imageQueue: Queue) {}
19 |
20 | @Post('image')
21 | @UseInterceptors(AnyFilesInterceptor())
22 | async processImage(@UploadedFiles() files: Express.Multer.File[]) {
23 | const job = await this.imageQueue.add('optimize', {
24 | files,
25 | });
26 |
27 | return {
28 | jobId: job.id,
29 | };
30 | }
31 |
32 | @Get('image/:id')
33 | async getJobResult(@Res() response: Response, @Param('id') id: string) {
34 | const job = await this.imageQueue.getJob(id);
35 |
36 | if (!job) {
37 | return response.sendStatus(404);
38 | }
39 |
40 | const isCompleted = await job.isCompleted();
41 |
42 | if (!isCompleted) {
43 | return response.sendStatus(202);
44 | }
45 |
46 | const result = Buffer.from(job.returnvalue);
47 |
48 | const stream = Readable.from(result);
49 |
50 | stream.pipe(response);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/optimize/optimize.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { OptimizeController } from './optimize.controller';
3 | import { BullModule } from '@nestjs/bull';
4 | import { join } from 'path';
5 |
6 | @Module({
7 | imports: [
8 | BullModule.registerQueue({
9 | name: 'image',
10 | processors: [
11 | {
12 | name: 'optimize',
13 | path: join(__dirname, 'image.processor.js'),
14 | },
15 | ],
16 | }),
17 | ],
18 | providers: [],
19 | exports: [],
20 | controllers: [OptimizeController],
21 | })
22 | export class OptimizeModule {}
23 |
--------------------------------------------------------------------------------
/src/posts/dto/createPost.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty } from 'class-validator';
2 |
3 | export class CreatePostDto {
4 | @IsString({ each: true })
5 | @IsNotEmpty()
6 | paragraphs: string[];
7 |
8 | @IsString()
9 | @IsNotEmpty()
10 | title: string;
11 | }
12 |
13 | export default CreatePostDto;
14 |
--------------------------------------------------------------------------------
/src/posts/dto/updatePost.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty, IsNumber, IsOptional } from 'class-validator';
2 |
3 | export class UpdatePostDto {
4 | @IsNumber()
5 | @IsOptional()
6 | id: number;
7 |
8 | @IsString({ each: true })
9 | @IsNotEmpty()
10 | @IsOptional()
11 | paragraphs: string[];
12 |
13 | @IsString()
14 | @IsNotEmpty()
15 | @IsOptional()
16 | title: string;
17 | }
18 |
19 | export default UpdatePostDto;
20 |
--------------------------------------------------------------------------------
/src/posts/exceptions/postNotFound.exception.ts:
--------------------------------------------------------------------------------
1 | import { NotFoundException } from '@nestjs/common';
2 |
3 | class PostNotFoundException extends NotFoundException {
4 | constructor(postId: number) {
5 | super(`Post with id ${postId} not found`);
6 | }
7 | }
8 |
9 | export default PostNotFoundException;
10 |
--------------------------------------------------------------------------------
/src/posts/httpCache.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CACHE_KEY_METADATA,
3 | CacheInterceptor,
4 | ExecutionContext,
5 | Injectable,
6 | } from '@nestjs/common';
7 |
8 | @Injectable()
9 | export class HttpCacheInterceptor extends CacheInterceptor {
10 | trackBy(context: ExecutionContext): string | undefined {
11 | const cacheKey = this.reflector.get(
12 | CACHE_KEY_METADATA,
13 | context.getHandler(),
14 | );
15 |
16 | if (cacheKey) {
17 | const request = context.switchToHttp().getRequest();
18 | return `${cacheKey}-${request._parsedUrl.query}`;
19 | }
20 |
21 | return super.trackBy(context);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/posts/inputs/post.input.ts:
--------------------------------------------------------------------------------
1 | import { InputType, Field } from '@nestjs/graphql';
2 |
3 | @InputType()
4 | export class CreatePostInput {
5 | @Field()
6 | title: string;
7 |
8 | @Field(() => [String])
9 | paragraphs: string[];
10 |
11 | @Field({ nullable: true })
12 | scheduledDate?: Date;
13 | }
14 |
--------------------------------------------------------------------------------
/src/posts/loaders/posts.loaders.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Scope } from '@nestjs/common';
2 | import { UsersService } from '../../users/users.service';
3 | import * as DataLoader from 'dataloader';
4 |
5 | @Injectable({ scope: Scope.REQUEST })
6 | export default class PostsLoaders {
7 | constructor(private usersService: UsersService) {}
8 |
9 | public readonly batchAuthors = new DataLoader(async (authorIds: number[]) => {
10 | const users = await this.usersService.getByIds(authorIds);
11 | const usersMap = new Map(users.map(user => [user.id, user]));
12 | return authorIds.map(authorId => usersMap.get(authorId));
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/src/posts/models/post.model.ts:
--------------------------------------------------------------------------------
1 | import { Field, Int, ObjectType } from '@nestjs/graphql';
2 | import { User } from '../../users/models/user.model';
3 |
4 | @ObjectType()
5 | export class Post {
6 | @Field(() => Int)
7 | id: number;
8 |
9 | @Field()
10 | title: string;
11 |
12 | @Field(() => [String])
13 | paragraphs: string[];
14 |
15 | @Field(() => Int)
16 | authorId: number;
17 |
18 | @Field()
19 | author: User;
20 |
21 | @Field()
22 | createdAt: Date;
23 |
24 | @Field({ nullable: true })
25 | scheduledDate?: Date;
26 | }
27 |
--------------------------------------------------------------------------------
/src/posts/post.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | Entity,
4 | JoinTable,
5 | ManyToMany,
6 | ManyToOne,
7 | PrimaryGeneratedColumn,
8 | Index,
9 | OneToMany,
10 | RelationId,
11 | CreateDateColumn,
12 | } from 'typeorm';
13 | import User from '../users/user.entity';
14 | import Category from '../categories/category.entity';
15 | import Comment from '../comments/comment.entity';
16 |
17 | @Entity()
18 | class Post {
19 | @PrimaryGeneratedColumn()
20 | public id: number;
21 |
22 | @Column()
23 | public title: string;
24 |
25 | @Column('text', { array: true })
26 | public paragraphs: string[];
27 |
28 | @Index('post_authorId_index')
29 | @ManyToOne(
30 | () => User,
31 | (author: User) => author.posts,
32 | )
33 | public author: User;
34 |
35 | @RelationId((post: Post) => post.author)
36 | public authorId: number;
37 |
38 | @ManyToMany(
39 | () => Category,
40 | (category: Category) => category.posts,
41 | )
42 | @JoinTable()
43 | public categories: Category[];
44 |
45 | @OneToMany(
46 | () => Comment,
47 | (comment: Comment) => comment.post,
48 | )
49 | public comments: Comment[];
50 |
51 | @CreateDateColumn({ type: 'timestamp' })
52 | createdAt: Date;
53 |
54 | @Column({
55 | type: 'timestamp',
56 | nullable: true,
57 | })
58 | scheduledDate?: Date;
59 | }
60 |
61 | export default Post;
62 |
--------------------------------------------------------------------------------
/src/posts/posts.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | Param,
7 | Patch,
8 | Post,
9 | UseGuards,
10 | Req,
11 | UseInterceptors,
12 | ClassSerializerInterceptor,
13 | Query,
14 | CacheKey,
15 | CacheTTL,
16 | } from '@nestjs/common';
17 | import PostEntity from './post.entity';
18 | import PostsService from './posts.service';
19 | import CreatePostDto from './dto/createPost.dto';
20 | import UpdatePostDto from './dto/updatePost.dto';
21 | import FindOneParams from '../utils/findOneParams';
22 | import RequestWithUser from '../authentication/requestWithUser.interface';
23 | import { PaginationParams } from '../utils/types/paginationParams';
24 | import { HttpCacheInterceptor } from './httpCache.interceptor';
25 | import { GET_POSTS_CACHE_KEY } from './postsCacheKey.constant';
26 | import JwtTwoFactorGuard from '../authentication/jwt-two-factor.guard';
27 | import { ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
28 |
29 | @Controller('posts')
30 | @ApiTags('posts')
31 | @UseInterceptors(ClassSerializerInterceptor)
32 | export default class PostsController {
33 | constructor(private readonly postsService: PostsService) {}
34 |
35 | @UseInterceptors(HttpCacheInterceptor)
36 | @CacheKey(GET_POSTS_CACHE_KEY)
37 | @CacheTTL(120)
38 | @Get()
39 | async getPosts(
40 | @Query('search') search: string,
41 | @Query() { offset, limit, startId }: PaginationParams,
42 | ) {
43 | if (search) {
44 | return this.postsService.searchForPosts(search, offset, limit, startId);
45 | }
46 | return this.postsService.getPostsWithAuthors(offset, limit, startId);
47 | }
48 |
49 | @Get(':id')
50 | @ApiParam({
51 | name: 'id',
52 | required: true,
53 | description: 'Should be an id of a post that exists in the database',
54 | type: Number,
55 | })
56 | @ApiResponse({
57 | status: 200,
58 | description: 'A post has been successfully fetched',
59 | type: PostEntity,
60 | })
61 | @ApiResponse({
62 | status: 404,
63 | description: 'A post with given id does not exist.',
64 | })
65 | getPostById(@Param() { id }: FindOneParams) {
66 | return this.postsService.getPostById(Number(id));
67 | }
68 |
69 | @Post()
70 | @UseGuards(JwtTwoFactorGuard)
71 | async createPost(@Body() post: CreatePostDto, @Req() req: RequestWithUser) {
72 | return this.postsService.createPost(post, req.user);
73 | }
74 |
75 | @Patch(':id')
76 | async updatePost(
77 | @Param() { id }: FindOneParams,
78 | @Body() post: UpdatePostDto,
79 | ) {
80 | return this.postsService.updatePost(Number(id), post);
81 | }
82 |
83 | @Delete(':id')
84 | async deletePost(@Param() { id }: FindOneParams) {
85 | return this.postsService.deletePost(Number(id));
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/posts/posts.module.ts:
--------------------------------------------------------------------------------
1 | import * as redisStore from 'cache-manager-redis-store';
2 | import { CacheModule, Module } from '@nestjs/common';
3 | import PostsController from './posts.controller';
4 | import PostsService from './posts.service';
5 | import Post from './post.entity';
6 | import { TypeOrmModule } from '@nestjs/typeorm';
7 | import { SearchModule } from '../search/search.module';
8 | import PostsSearchService from './postsSearch.service';
9 | import { ConfigModule, ConfigService } from '@nestjs/config';
10 | import { PostsResolver } from './posts.resolver';
11 | import { UsersModule } from '../users/users.module';
12 | import PostsLoaders from './loaders/posts.loaders';
13 |
14 | @Module({
15 | imports: [
16 | CacheModule.registerAsync({
17 | imports: [ConfigModule],
18 | inject: [ConfigService],
19 | useFactory: (configService: ConfigService) => ({
20 | store: redisStore,
21 | host: configService.get('REDIS_HOST'),
22 | port: configService.get('REDIS_PORT'),
23 | ttl: 120,
24 | }),
25 | }),
26 | TypeOrmModule.forFeature([Post]),
27 | SearchModule,
28 | UsersModule,
29 | ],
30 | controllers: [PostsController],
31 | providers: [PostsService, PostsSearchService, PostsResolver, PostsLoaders],
32 | })
33 | export class PostsModule {}
34 |
--------------------------------------------------------------------------------
/src/posts/posts.resolver.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Args,
3 | Context,
4 | Info,
5 | Mutation,
6 | Query,
7 | Resolver,
8 | Subscription,
9 | } from '@nestjs/graphql';
10 | import { Post } from './models/post.model';
11 | import PostsService from './posts.service';
12 | import { CreatePostInput } from './inputs/post.input';
13 | import { Inject, UseGuards } from '@nestjs/common';
14 | import RequestWithUser from '../authentication/requestWithUser.interface';
15 | import { GraphqlJwtAuthGuard } from '../authentication/graphql-jwt-auth.guard';
16 | import {
17 | parseResolveInfo,
18 | ResolveTree,
19 | simplifyParsedResolveInfoFragmentWithType,
20 | } from 'graphql-parse-resolve-info';
21 | import { GraphQLResolveInfo } from 'graphql';
22 | import { RedisPubSub } from 'graphql-redis-subscriptions';
23 | import { PUB_SUB } from '../pubSub/pubSub.module';
24 |
25 | const POST_ADDED_EVENT = 'postAdded';
26 |
27 | @Resolver(() => Post)
28 | export class PostsResolver {
29 | constructor(
30 | private postsService: PostsService,
31 | @Inject(PUB_SUB) private pubSub: RedisPubSub,
32 | ) {}
33 |
34 | @Query(() => [Post])
35 | async posts(@Info() info: GraphQLResolveInfo) {
36 | const parsedInfo = parseResolveInfo(info) as ResolveTree;
37 | const simplifiedInfo = simplifyParsedResolveInfoFragmentWithType(
38 | parsedInfo,
39 | info.returnType,
40 | );
41 |
42 | const posts =
43 | 'author' in simplifiedInfo.fields
44 | ? await this.postsService.getPostsWithAuthors()
45 | : await this.postsService.getPosts();
46 |
47 | return posts.items;
48 | }
49 |
50 | @Subscription(() => Post)
51 | postAdded() {
52 | return this.pubSub.asyncIterator(POST_ADDED_EVENT);
53 | }
54 |
55 | @Mutation(() => Post)
56 | @UseGuards(GraphqlJwtAuthGuard)
57 | async createPost(
58 | @Args('input') createPostInput: CreatePostInput,
59 | @Context() context: { req: RequestWithUser },
60 | ) {
61 | const newPost = await this.postsService.createPost(
62 | createPostInput,
63 | context.req.user,
64 | );
65 | this.pubSub.publish(POST_ADDED_EVENT, { postAdded: newPost });
66 | return newPost;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/posts/postsCacheKey.constant.ts:
--------------------------------------------------------------------------------
1 | export const GET_POSTS_CACHE_KEY = 'GET_POSTS_CACHE';
2 |
--------------------------------------------------------------------------------
/src/posts/types/postCountBody.interface.ts:
--------------------------------------------------------------------------------
1 | interface PostCountResult {
2 | count: number;
3 | }
4 |
5 | export default PostCountResult;
6 |
--------------------------------------------------------------------------------
/src/posts/types/postSearchBody.interface.ts:
--------------------------------------------------------------------------------
1 | interface PostSearchBody {
2 | id: number;
3 | title: string;
4 | paragraphs: string[];
5 | authorId: number;
6 | }
7 |
8 | export default PostSearchBody;
9 |
--------------------------------------------------------------------------------
/src/posts/types/postSearchResponse.interface.ts:
--------------------------------------------------------------------------------
1 | import PostSearchBody from './postSearchBody.interface';
2 |
3 | interface PostSearchResult {
4 | hits: {
5 | total: {
6 | value: number;
7 | };
8 | hits: Array<{
9 | _source: PostSearchBody;
10 | }>;
11 | };
12 | }
13 |
14 | export default PostSearchResult;
15 |
--------------------------------------------------------------------------------
/src/productCategories/dto/createProductCategory.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty } from 'class-validator';
2 |
3 | export class CreateProductCategoryDto {
4 | @IsString()
5 | @IsNotEmpty()
6 | name: string;
7 | }
8 |
9 | export default CreateProductCategoryDto;
10 |
--------------------------------------------------------------------------------
/src/productCategories/productCategories.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Get,
5 | UseGuards,
6 | UseInterceptors,
7 | ClassSerializerInterceptor,
8 | Post,
9 | } from '@nestjs/common';
10 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
11 | import ProductCategoriesService from './productCategories.service';
12 | import CreateProductCategoryDto from './dto/createProductCategory.dto';
13 |
14 | @Controller('product-categories')
15 | @UseInterceptors(ClassSerializerInterceptor)
16 | export default class ProductCategoriesController {
17 | constructor(private readonly productsService: ProductCategoriesService) {}
18 |
19 | @Get()
20 | getAllProducts() {
21 | return this.productsService.getAllProductCategories();
22 | }
23 |
24 | @Post()
25 | @UseGuards(JwtAuthenticationGuard)
26 | async createProduct(@Body() productCategory: CreateProductCategoryDto) {
27 | return this.productsService.createProductCategory(productCategory);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/productCategories/productCategories.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import ProductCategory from './productCategory.entity';
4 | import ProductCategoriesController from './productCategories.controller';
5 | import ProductCategoriesService from './productCategories.service';
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([ProductCategory])],
9 | controllers: [ProductCategoriesController],
10 | providers: [ProductCategoriesService],
11 | })
12 | export class ProductCategoriesModule {}
13 |
--------------------------------------------------------------------------------
/src/productCategories/productCategories.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository } from 'typeorm';
4 | import ProductCategory from './productCategory.entity';
5 | import CreateProductCategoryDto from './dto/createProductCategory.dto';
6 |
7 | @Injectable()
8 | export default class ProductCategoriesService {
9 | constructor(
10 | @InjectRepository(ProductCategory)
11 | private productCategoriesRepository: Repository,
12 | ) {}
13 |
14 | getAllProductCategories() {
15 | return this.productCategoriesRepository.find();
16 | }
17 |
18 | async createProductCategory(category: CreateProductCategoryDto) {
19 | const newProductCategory = await this.productCategoriesRepository.create(
20 | category,
21 | );
22 | await this.productCategoriesRepository.save(newProductCategory);
23 | return newProductCategory;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/productCategories/productCategory.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
2 | import Product from '../products/product.entity';
3 |
4 | @Entity()
5 | class ProductCategory {
6 | @PrimaryGeneratedColumn()
7 | public id: number;
8 |
9 | @Column()
10 | public name: string;
11 |
12 | @OneToMany(
13 | () => Product,
14 | (product: Product) => product.category,
15 | )
16 | public products: Product[];
17 | }
18 |
19 | export default ProductCategory;
20 |
--------------------------------------------------------------------------------
/src/products/dto/createProduct.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty, ValidateNested } from 'class-validator';
2 | import { Type } from 'class-transformer';
3 | import ObjectWithIdDTO from '../../utils/types/objectWithId.dto';
4 |
5 | export class CreateProductDto {
6 | @IsString()
7 | @IsNotEmpty()
8 | name: string;
9 |
10 | @ValidateNested()
11 | @Type(() => ObjectWithIdDTO)
12 | category: ObjectWithIdDTO;
13 | }
14 |
15 | export default CreateProductDto;
16 |
--------------------------------------------------------------------------------
/src/products/product.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
2 | import ProductCategory from '../productCategories/productCategory.entity';
3 | import { CarProperties } from './types/carProperties.interface';
4 | import { BookProperties } from './types/bookProperties.interface';
5 |
6 | @Entity()
7 | class Product {
8 | @PrimaryGeneratedColumn()
9 | public id: number;
10 |
11 | @Column()
12 | public name: string;
13 |
14 | @ManyToOne(
15 | () => ProductCategory,
16 | (category: ProductCategory) => category.products,
17 | )
18 | public category: ProductCategory;
19 |
20 | @Column({
21 | type: 'jsonb',
22 | })
23 | public properties: CarProperties | BookProperties;
24 | }
25 |
26 | export default Product;
27 |
--------------------------------------------------------------------------------
/src/products/products.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Get,
5 | UseGuards,
6 | UseInterceptors,
7 | ClassSerializerInterceptor,
8 | Post,
9 | } from '@nestjs/common';
10 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
11 | import CreateProductDto from './dto/createProduct.dto';
12 | import ProductsService from './products.service';
13 |
14 | @Controller('products')
15 | @UseInterceptors(ClassSerializerInterceptor)
16 | export default class ProductsController {
17 | constructor(private readonly productsService: ProductsService) {}
18 |
19 | @Get()
20 | getAllProducts() {
21 | return this.productsService.getAllProducts();
22 | }
23 |
24 | @Post()
25 | @UseGuards(JwtAuthenticationGuard)
26 | async createProduct(@Body() product: CreateProductDto) {
27 | return this.productsService.createProduct(product);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/products/products.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import Product from './product.entity';
4 | import ProductsController from './products.controller';
5 | import ProductsService from './products.service';
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature([Product])],
9 | controllers: [ProductsController],
10 | providers: [ProductsService],
11 | })
12 | export class ProductsModule {}
13 |
--------------------------------------------------------------------------------
/src/products/products.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository } from 'typeorm';
4 | import Product from './product.entity';
5 | import CreateProductDto from './dto/createProduct.dto';
6 |
7 | @Injectable()
8 | export default class ProductsService {
9 | constructor(
10 | @InjectRepository(Product)
11 | private productsRepository: Repository,
12 | ) {}
13 |
14 | getAllProducts() {
15 | return this.productsRepository.find();
16 | }
17 |
18 | async createProduct(product: CreateProductDto) {
19 | const newProduct = await this.productsRepository.create(product);
20 | await this.productsRepository.save(newProduct);
21 | return newProduct;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/products/types/bookProperties.interface.ts:
--------------------------------------------------------------------------------
1 | export interface BookProperties {
2 | authors: string[];
3 | publicationYear: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/products/types/carProperties.interface.ts:
--------------------------------------------------------------------------------
1 | export interface CarProperties {
2 | brand: string;
3 | engine: {
4 | fuel: string;
5 | numberOfCylinders: number;
6 | };
7 | }
8 |
--------------------------------------------------------------------------------
/src/pubSub/pubSub.module.ts:
--------------------------------------------------------------------------------
1 | import { ConfigModule, ConfigService } from '@nestjs/config';
2 | import { RedisPubSub } from 'graphql-redis-subscriptions';
3 | import { Global, Module } from '@nestjs/common';
4 |
5 | export const PUB_SUB = 'PUB_SUB';
6 |
7 | @Global()
8 | @Module({
9 | imports: [ConfigModule],
10 | providers: [
11 | {
12 | provide: PUB_SUB,
13 | useFactory: (configService: ConfigService) =>
14 | new RedisPubSub({
15 | connection: {
16 | host: configService.get('REDIS_HOST'),
17 | port: configService.get('REDIS_PORT'),
18 | },
19 | }),
20 | inject: [ConfigService],
21 | },
22 | ],
23 | exports: [PUB_SUB],
24 | })
25 | export class PubSubModule {}
26 |
--------------------------------------------------------------------------------
/src/repl.ts:
--------------------------------------------------------------------------------
1 | import { repl } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import { UsersService } from './users/users.service';
4 | import CreatePostDto from './posts/dto/createPost.dto';
5 | import User from './users/user.entity';
6 | import PostsService from './posts/posts.service';
7 |
8 | async function bootstrap() {
9 | const replServer = await repl(AppModule);
10 | replServer.context.getUserByEmail = (userEmail: string) => {
11 | const usersService: UsersService = replServer.context.get(UsersService);
12 | return usersService.getByEmail(userEmail);
13 | };
14 | replServer.context.createPost = (post: CreatePostDto, user: User) => {
15 | const postsService: PostsService = replServer.context.get(PostsService);
16 | return postsService.createPost(post, user);
17 | };
18 | }
19 | bootstrap();
20 |
--------------------------------------------------------------------------------
/src/schema.gql:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------
2 | # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
3 | # ------------------------------------------------------
4 |
5 | type User {
6 | id: Int!
7 | email: String!
8 | }
9 |
10 | type Post {
11 | id: Int!
12 | title: String!
13 | paragraphs: [String!]!
14 | authorId: Int!
15 | author: User!
16 | createdAt: Timestamp!
17 | scheduledDate: Timestamp
18 | }
19 |
20 | """
21 | `Date` type as integer. Type represents date and time as number of milliseconds from start of UNIX epoch.
22 | """
23 | scalar Timestamp
24 |
25 | type Query {
26 | posts: [Post!]!
27 | }
28 |
29 | type Mutation {
30 | createPost(input: CreatePostInput!): Post!
31 | }
32 |
33 | input CreatePostInput {
34 | title: String!
35 | paragraphs: [String!]!
36 | scheduledDate: Timestamp
37 | }
38 |
39 | type Subscription {
40 | postAdded: Post!
41 | }
42 |
--------------------------------------------------------------------------------
/src/search/search.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule, ConfigService } from '@nestjs/config';
3 | import { ElasticsearchModule } from '@nestjs/elasticsearch';
4 |
5 | @Module({
6 | imports: [
7 | ConfigModule,
8 | ElasticsearchModule.registerAsync({
9 | imports: [ConfigModule],
10 | useFactory: async (configService: ConfigService) => ({
11 | node: configService.get('ELASTICSEARCH_NODE'),
12 | auth: {
13 | username: configService.get('ELASTICSEARCH_USERNAME'),
14 | password: configService.get('ELASTICSEARCH_PASSWORD'),
15 | },
16 | }),
17 | inject: [ConfigService],
18 | }),
19 | ],
20 | exports: [ElasticsearchModule],
21 | })
22 | export class SearchModule {}
23 |
--------------------------------------------------------------------------------
/src/sms/checkVerificationCode.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty } from 'class-validator';
2 |
3 | export class CheckVerificationCodeDto {
4 | @IsString()
5 | @IsNotEmpty()
6 | code: string;
7 | }
8 |
9 | export default CheckVerificationCodeDto;
10 |
--------------------------------------------------------------------------------
/src/sms/sms.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | UseGuards,
5 | UseInterceptors,
6 | ClassSerializerInterceptor,
7 | Post,
8 | Req,
9 | BadRequestException,
10 | } from '@nestjs/common';
11 | import SmsService from './sms.service';
12 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
13 | import RequestWithUser from '../authentication/requestWithUser.interface';
14 | import CheckVerificationCodeDto from './checkVerificationCode.dto';
15 |
16 | @Controller('sms')
17 | @UseInterceptors(ClassSerializerInterceptor)
18 | export default class SmsController {
19 | constructor(private readonly smsService: SmsService) {}
20 |
21 | @Post('initiate-verification')
22 | @UseGuards(JwtAuthenticationGuard)
23 | async initiatePhoneNumberVerification(@Req() request: RequestWithUser) {
24 | if (request.user.isPhoneNumberConfirmed) {
25 | throw new BadRequestException('Phone number already confirmed');
26 | }
27 | await this.smsService.initiatePhoneNumberVerification(
28 | request.user.phoneNumber,
29 | );
30 | }
31 |
32 | @Post('check-verification-code')
33 | @UseGuards(JwtAuthenticationGuard)
34 | async checkVerificationCode(
35 | @Req() request: RequestWithUser,
36 | @Body() verificationData: CheckVerificationCodeDto,
37 | ) {
38 | if (request.user.isPhoneNumberConfirmed) {
39 | throw new BadRequestException('Phone number already confirmed');
40 | }
41 | await this.smsService.confirmPhoneNumber(
42 | request.user.id,
43 | request.user.phoneNumber,
44 | verificationData.code,
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/sms/sms.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule } from '@nestjs/config';
3 | import SmsService from './sms.service';
4 | import SmsController from './sms.controller';
5 | import { UsersModule } from '../users/users.module';
6 |
7 | @Module({
8 | imports: [ConfigModule, UsersModule],
9 | controllers: [SmsController],
10 | providers: [SmsService],
11 | exports: [SmsService],
12 | })
13 | export class SmsModule {}
14 |
--------------------------------------------------------------------------------
/src/sms/sms.service.ts:
--------------------------------------------------------------------------------
1 | import { BadRequestException, Injectable } from '@nestjs/common';
2 | import { ConfigService } from '@nestjs/config';
3 | import { Twilio } from 'twilio';
4 | import { UsersService } from '../users/users.service';
5 |
6 | @Injectable()
7 | export default class SmsService {
8 | private twilioClient: Twilio;
9 |
10 | constructor(
11 | private readonly configService: ConfigService,
12 | private readonly usersService: UsersService,
13 | ) {
14 | const accountSid = configService.get('TWILIO_ACCOUNT_SID');
15 | const authToken = configService.get('TWILIO_AUTH_TOKEN');
16 |
17 | this.twilioClient = new Twilio(accountSid, authToken);
18 | }
19 |
20 | initiatePhoneNumberVerification(phoneNumber: string) {
21 | const serviceSid = this.configService.get(
22 | 'TWILIO_VERIFICATION_SERVICE_SID',
23 | );
24 |
25 | return this.twilioClient.verify
26 | .services(serviceSid)
27 | .verifications.create({ to: phoneNumber, channel: 'sms' });
28 | }
29 |
30 | async confirmPhoneNumber(
31 | userId: number,
32 | phoneNumber: string,
33 | verificationCode: string,
34 | ) {
35 | const serviceSid = this.configService.get(
36 | 'TWILIO_VERIFICATION_SERVICE_SID',
37 | );
38 |
39 | const result = await this.twilioClient.verify
40 | .services(serviceSid)
41 | .verificationChecks.create({ to: phoneNumber, code: verificationCode });
42 |
43 | if (!result.valid || result.status !== 'approved') {
44 | throw new BadRequestException('Wrong code provided');
45 | }
46 |
47 | await this.usersService.markPhoneNumberAsConfirmed(userId);
48 | }
49 |
50 | async sendMessage(receiverPhoneNumber: string, message: string) {
51 | const senderPhoneNumber = this.configService.get(
52 | 'TWILIO_SENDER_PHONE_NUMBER',
53 | );
54 |
55 | return this.twilioClient.messages.create({
56 | body: message,
57 | from: senderPhoneNumber,
58 | to: receiverPhoneNumber,
59 | });
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/stripe/stripe.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import StripeService from './stripe.service';
3 | import { ConfigModule } from '@nestjs/config';
4 |
5 | @Module({
6 | imports: [ConfigModule],
7 | providers: [StripeService],
8 | exports: [StripeService],
9 | })
10 | export class StripeModule {}
11 |
--------------------------------------------------------------------------------
/src/stripeWebhook/StripeEvent.entity.ts:
--------------------------------------------------------------------------------
1 | import { Entity, PrimaryColumn } from 'typeorm';
2 |
3 | @Entity()
4 | class StripeEvent {
5 | @PrimaryColumn()
6 | public id: string;
7 | }
8 |
9 | export default StripeEvent;
10 |
--------------------------------------------------------------------------------
/src/stripeWebhook/requestWithRawBody.interface.ts:
--------------------------------------------------------------------------------
1 | import { Request } from 'express';
2 |
3 | interface RequestWithRawBody extends Request {
4 | rawBody: Buffer;
5 | }
6 |
7 | export default RequestWithRawBody;
8 |
--------------------------------------------------------------------------------
/src/stripeWebhook/stripeWebhook.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Post,
4 | Headers,
5 | Req,
6 | BadRequestException,
7 | } from '@nestjs/common';
8 | import StripeService from '../stripe/stripe.service';
9 | import RequestWithRawBody from './requestWithRawBody.interface';
10 | import { UsersService } from '../users/users.service';
11 | import StripeWebhookService from './stripeWebhook.service';
12 |
13 | @Controller('webhook')
14 | export default class StripeWebhookController {
15 | constructor(
16 | private readonly stripeService: StripeService,
17 | private readonly usersService: UsersService,
18 | private readonly stripeWebhookService: StripeWebhookService,
19 | ) {}
20 |
21 | @Post()
22 | async handleIncomingEvents(
23 | @Headers('stripe-signature') signature: string,
24 | @Req() request: RequestWithRawBody,
25 | ) {
26 | if (!signature) {
27 | throw new BadRequestException('Missing stripe-signature header');
28 | }
29 |
30 | const event = await this.stripeService.constructEventFromPayload(
31 | signature,
32 | request.rawBody,
33 | );
34 |
35 | if (
36 | event.type === 'customer.subscription.updated' ||
37 | event.type === 'customer.subscription.created'
38 | ) {
39 | return this.stripeWebhookService.processSubscriptionUpdate(event);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/stripeWebhook/stripeWebhook.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import StripeWebhookController from './stripeWebhook.controller';
3 | import { StripeModule } from '../stripe/stripe.module';
4 | import { UsersModule } from '../users/users.module';
5 | import { TypeOrmModule } from '@nestjs/typeorm';
6 | import StripeEvent from './StripeEvent.entity';
7 | import StripeWebhookService from './stripeWebhook.service';
8 |
9 | @Module({
10 | imports: [StripeModule, UsersModule, TypeOrmModule.forFeature([StripeEvent])],
11 | controllers: [StripeWebhookController],
12 | providers: [StripeWebhookService],
13 | })
14 | export class StripeWebhookModule {}
15 |
--------------------------------------------------------------------------------
/src/stripeWebhook/stripeWebhook.service.ts:
--------------------------------------------------------------------------------
1 | import { BadRequestException, Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import StripeEvent from './StripeEvent.entity';
4 | import { Repository } from 'typeorm';
5 | import Stripe from 'stripe';
6 | import PostgresErrorCode from '../database/postgresErrorCode.enum';
7 | import { UsersService } from '../users/users.service';
8 |
9 | @Injectable()
10 | export default class StripeWebhookService {
11 | constructor(
12 | @InjectRepository(StripeEvent)
13 | private eventsRepository: Repository,
14 | private readonly usersService: UsersService,
15 | ) {}
16 |
17 | createEvent(id: string) {
18 | return this.eventsRepository.insert({ id });
19 | }
20 |
21 | async processSubscriptionUpdate(event: Stripe.Event) {
22 | try {
23 | await this.createEvent(event.id);
24 | } catch (error) {
25 | if (error?.code === PostgresErrorCode.UniqueViolation) {
26 | throw new BadRequestException('This event was already processed');
27 | }
28 | }
29 |
30 | const data = event.data.object as Stripe.Subscription;
31 |
32 | const customerId: string = data.customer as string;
33 | const subscriptionStatus = data.status;
34 |
35 | await this.usersService.updateMonthlySubscriptionStatus(
36 | customerId,
37 | subscriptionStatus,
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/subscribers/dto/createSubscriber.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateSubscriberDto {
2 | email: string;
3 | name: string;
4 | }
5 |
6 | export default CreateSubscriberDto;
7 |
--------------------------------------------------------------------------------
/src/subscribers/subscriber.service.ts:
--------------------------------------------------------------------------------
1 | export interface Subscriber {
2 | id: number;
3 | email: string;
4 | name: string;
5 | }
6 |
7 | export default Subscriber;
8 |
--------------------------------------------------------------------------------
/src/subscribers/subscribers.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Get,
5 | Post,
6 | UseGuards,
7 | UseInterceptors,
8 | ClassSerializerInterceptor,
9 | Inject,
10 | OnModuleInit,
11 | } from '@nestjs/common';
12 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
13 | import CreateSubscriberDto from './dto/createSubscriber.dto';
14 | import { ClientGrpc } from '@nestjs/microservices';
15 | import SubscribersService from './subscribers.service.interface';
16 |
17 | @Controller('subscribers')
18 | @UseInterceptors(ClassSerializerInterceptor)
19 | export default class SubscribersController implements OnModuleInit {
20 | private subscribersService: SubscribersService;
21 |
22 | constructor(@Inject('SUBSCRIBERS_PACKAGE') private client: ClientGrpc) {}
23 |
24 | onModuleInit() {
25 | this.subscribersService = this.client.getService(
26 | 'SubscribersService',
27 | );
28 | }
29 |
30 | @Get()
31 | async getSubscribers() {
32 | return this.subscribersService.getAllSubscribers({});
33 | }
34 |
35 | @Post()
36 | @UseGuards(JwtAuthenticationGuard)
37 | async createPost(@Body() subscriber: CreateSubscriberDto) {
38 | return this.subscribersService.addSubscriber(subscriber);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/subscribers/subscribers.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import SubscribersController from './subscribers.controller';
3 | import { ConfigModule, ConfigService } from '@nestjs/config';
4 | import { ClientProxyFactory, Transport } from '@nestjs/microservices';
5 | import { join } from 'path';
6 |
7 | @Module({
8 | imports: [ConfigModule],
9 | controllers: [SubscribersController],
10 | providers: [
11 | {
12 | provide: 'SUBSCRIBERS_PACKAGE',
13 | useFactory: (configService: ConfigService) => {
14 | return ClientProxyFactory.create({
15 | transport: Transport.GRPC,
16 | options: {
17 | package: 'subscribers',
18 | protoPath: join(process.cwd(), 'src/subscribers/subscribers.proto'),
19 | url: configService.get('GRPC_CONNECTION_URL'),
20 | },
21 | });
22 | },
23 | inject: [ConfigService],
24 | },
25 | ],
26 | })
27 | export class SubscribersModule {}
28 |
--------------------------------------------------------------------------------
/src/subscribers/subscribers.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package subscribers;
4 |
5 | service SubscribersService {
6 | rpc GetAllSubscribers (GetAllSubscribersParams) returns (SubscribersResponse) {}
7 | rpc AddSubscriber (CreateSubscriberDto) returns (Subscriber) {}
8 | }
9 |
10 | message GetAllSubscribersParams {}
11 |
12 | message SubscribersResponse {
13 | repeated Subscriber data = 1;
14 | }
15 |
16 | message Subscriber {
17 | int32 id = 1;
18 | string email = 2;
19 | string name = 3;
20 | }
21 |
22 | message CreateSubscriberDto {
23 | string email = 1;
24 | string name = 2;
25 | }
--------------------------------------------------------------------------------
/src/subscribers/subscribers.service.interface.ts:
--------------------------------------------------------------------------------
1 | import CreateSubscriberDto from './dto/createSubscriber.dto';
2 | import Subscriber from './subscriber.service';
3 |
4 | interface SubscribersService {
5 | addSubscriber(subscriber: CreateSubscriberDto): Promise;
6 | getAllSubscribers(params: {}): Promise;
7 | }
8 |
9 | export default SubscribersService;
10 |
--------------------------------------------------------------------------------
/src/subscriptions/subscriptions.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Req, UseGuards, Get } from '@nestjs/common';
2 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
3 | import RequestWithUser from '../authentication/requestWithUser.interface';
4 | import SubscriptionsService from './subscriptions.service';
5 |
6 | @Controller('subscriptions')
7 | export default class SubscriptionsController {
8 | constructor(private readonly subscriptionsService: SubscriptionsService) {}
9 |
10 | @Post('monthly')
11 | @UseGuards(JwtAuthenticationGuard)
12 | async createMonthlySubscription(@Req() request: RequestWithUser) {
13 | return this.subscriptionsService.createMonthlySubscription(
14 | request.user.stripeCustomerId,
15 | );
16 | }
17 |
18 | @Get('monthly')
19 | @UseGuards(JwtAuthenticationGuard)
20 | async getMonthlySubscription(@Req() request: RequestWithUser) {
21 | return this.subscriptionsService.getMonthlySubscription(
22 | request.user.stripeCustomerId,
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/subscriptions/subscriptions.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { StripeModule } from '../stripe/stripe.module';
3 | import { ConfigModule } from '@nestjs/config';
4 | import SubscriptionsController from './subscriptions.controller';
5 | import SubscriptionsService from './subscriptions.service';
6 |
7 | @Module({
8 | imports: [StripeModule, ConfigModule],
9 | controllers: [SubscriptionsController],
10 | providers: [SubscriptionsService],
11 | })
12 | export class SubscriptionsModule {}
13 |
--------------------------------------------------------------------------------
/src/subscriptions/subscriptions.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BadRequestException,
3 | Injectable,
4 | NotFoundException,
5 | } from '@nestjs/common';
6 | import StripeService from '../stripe/stripe.service';
7 | import { ConfigService } from '@nestjs/config';
8 |
9 | @Injectable()
10 | export default class SubscriptionsService {
11 | constructor(
12 | private readonly stripeService: StripeService,
13 | private readonly configService: ConfigService,
14 | ) {}
15 |
16 | public async createMonthlySubscription(customerId: string) {
17 | const priceId = this.configService.get('MONTHLY_SUBSCRIPTION_PRICE_ID');
18 |
19 | const subscriptions = await this.stripeService.listSubscriptions(
20 | priceId,
21 | customerId,
22 | );
23 | if (subscriptions.data.length) {
24 | throw new BadRequestException('Customer already subscribed');
25 | }
26 | return this.stripeService.createSubscription(priceId, customerId);
27 | }
28 |
29 | public async getMonthlySubscription(customerId: string) {
30 | const priceId = this.configService.get('MONTHLY_SUBSCRIPTION_PRICE_ID');
31 | const subscriptions = await this.stripeService.listSubscriptions(
32 | priceId,
33 | customerId,
34 | );
35 |
36 | if (!subscriptions.data.length) {
37 | return new NotFoundException('Customer not subscribed');
38 | }
39 | return subscriptions.data[0];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/users/address.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
2 | import User from './user.entity';
3 |
4 | @Entity()
5 | class Address {
6 | @PrimaryGeneratedColumn()
7 | public id: number;
8 |
9 | @Column()
10 | public street: string;
11 |
12 | @Column()
13 | public city: string;
14 |
15 | @Column()
16 | public country: string;
17 |
18 | @OneToOne(
19 | () => User,
20 | (user: User) => user.address,
21 | )
22 | public user?: User;
23 | }
24 |
25 | export default Address;
26 |
--------------------------------------------------------------------------------
/src/users/dto/createUser.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateUserDto {
2 | email: string;
3 | name: string;
4 | password: string;
5 | }
6 |
7 | export default CreateUserDto;
8 |
--------------------------------------------------------------------------------
/src/users/dto/fileUpload.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { Express } from 'express';
3 |
4 | class FileUploadDto {
5 | @ApiProperty({ type: 'string', format: 'binary' })
6 | file: Express.Multer.File;
7 | }
8 |
9 | export default FileUploadDto;
10 |
--------------------------------------------------------------------------------
/src/users/models/user.model.ts:
--------------------------------------------------------------------------------
1 | import { Field, Int, ObjectType } from '@nestjs/graphql';
2 |
3 | @ObjectType()
4 | export class User {
5 | @Field(() => Int)
6 | id: number;
7 |
8 | @Field()
9 | email: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/users/tests/users.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { getRepositoryToken } from '@nestjs/typeorm';
3 | import User from '../../users/user.entity';
4 | import { UsersService } from '../../users/users.service';
5 |
6 | describe('The UsersService', () => {
7 | let usersService: UsersService;
8 | let findOne: jest.Mock;
9 | beforeEach(async () => {
10 | findOne = jest.fn();
11 | const module = await Test.createTestingModule({
12 | providers: [
13 | UsersService,
14 | {
15 | provide: getRepositoryToken(User),
16 | useValue: {
17 | findOne,
18 | },
19 | },
20 | ],
21 | }).compile();
22 | usersService = await module.get(UsersService);
23 | });
24 | describe('when getting a user by email', () => {
25 | describe('and the user is matched', () => {
26 | let user: User;
27 | beforeEach(() => {
28 | user = new User();
29 | findOne.mockReturnValue(Promise.resolve(user));
30 | });
31 | it('should return the user', async () => {
32 | const fetchedUser = await usersService.getByEmail('test@test.com');
33 | expect(fetchedUser).toEqual(user);
34 | });
35 | });
36 | describe('and the user is not matched', () => {
37 | beforeEach(() => {
38 | findOne.mockReturnValue(undefined);
39 | });
40 | it('should throw an error', async () => {
41 | await expect(
42 | usersService.getByEmail('test@test.com'),
43 | ).rejects.toThrow();
44 | });
45 | });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/src/users/user.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Column,
3 | Entity,
4 | JoinColumn,
5 | OneToMany,
6 | OneToOne,
7 | PrimaryGeneratedColumn,
8 | } from 'typeorm';
9 | import { Exclude } from 'class-transformer';
10 | import Address from './address.entity';
11 | import Post from '../posts/post.entity';
12 | import LocalFile from '../localFiles/localFile.entity';
13 |
14 | @Entity()
15 | class User {
16 | @PrimaryGeneratedColumn()
17 | public id: number;
18 |
19 | @Column({ unique: true })
20 | public email: string;
21 |
22 | @Column({ nullable: true })
23 | public phoneNumber?: string;
24 |
25 | @Column()
26 | public name: string;
27 |
28 | @Column({ nullable: true })
29 | @Exclude()
30 | public password?: string;
31 |
32 | @Column({ default: false })
33 | public isRegisteredWithGoogle: boolean;
34 |
35 | @OneToOne(() => Address, {
36 | eager: true,
37 | cascade: true,
38 | })
39 | @JoinColumn()
40 | public address: Address;
41 |
42 | @OneToMany(
43 | () => Post,
44 | (post: Post) => post.author,
45 | )
46 | public posts?: Post[];
47 |
48 | @JoinColumn({ name: 'avatarId' })
49 | @OneToOne(() => LocalFile, {
50 | nullable: true,
51 | })
52 | public avatar?: LocalFile;
53 |
54 | @Column({ nullable: true })
55 | public avatarId?: number;
56 |
57 | @Column({
58 | nullable: true,
59 | })
60 | @Exclude()
61 | public currentHashedRefreshToken?: string;
62 |
63 | @Column({ nullable: true })
64 | public twoFactorAuthenticationSecret?: string;
65 |
66 | @Column({ default: false })
67 | public isTwoFactorAuthenticationEnabled: boolean;
68 |
69 | @Column()
70 | public stripeCustomerId: string;
71 |
72 | @Column({ nullable: true })
73 | public monthlySubscriptionStatus?: string;
74 |
75 | @Column({ default: false })
76 | public isEmailConfirmed: boolean;
77 |
78 | @Column({ default: false })
79 | public isPhoneNumberConfirmed: boolean;
80 | }
81 |
82 | export default User;
83 |
--------------------------------------------------------------------------------
/src/users/users.controller.ts:
--------------------------------------------------------------------------------
1 | import { UsersService } from './users.service';
2 | import {
3 | BadRequestException,
4 | Controller,
5 | Post,
6 | Req,
7 | UploadedFile,
8 | UseGuards,
9 | UseInterceptors,
10 | } from '@nestjs/common';
11 | import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
12 | import RequestWithUser from '../authentication/requestWithUser.interface';
13 | import { Express } from 'express';
14 | import LocalFilesInterceptor from '../localFiles/localFiles.interceptor';
15 | import { ApiBody, ApiConsumes } from '@nestjs/swagger';
16 | import FileUploadDto from './dto/fileUpload.dto';
17 |
18 | @Controller('users')
19 | export class UsersController {
20 | constructor(private readonly usersService: UsersService) {}
21 |
22 | @Post('avatar')
23 | @UseGuards(JwtAuthenticationGuard)
24 | @UseInterceptors(
25 | LocalFilesInterceptor({
26 | fieldName: 'file',
27 | path: '/avatars',
28 | fileFilter: (request, file, callback) => {
29 | if (!file.mimetype.includes('image')) {
30 | return callback(
31 | new BadRequestException('Provide a valid image'),
32 | false,
33 | );
34 | }
35 | callback(null, true);
36 | },
37 | limits: {
38 | fileSize: Math.pow(1024, 2), // 1MB
39 | },
40 | }),
41 | )
42 | @ApiConsumes('multipart/form-data')
43 | @ApiBody({
44 | description: 'A new avatar for the user',
45 | type: FileUploadDto,
46 | })
47 | async addAvatar(
48 | @Req() request: RequestWithUser,
49 | @UploadedFile() file: Express.Multer.File,
50 | ) {
51 | return this.usersService.addAvatar(request.user.id, {
52 | path: file.path,
53 | filename: file.originalname,
54 | mimetype: file.mimetype,
55 | });
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/users/users.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { UsersService } from './users.service';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import User from './user.entity';
5 | import { UsersController } from './users.controller';
6 | import { StripeModule } from '../stripe/stripe.module';
7 | import { DatabaseFilesModule } from '../databaseFiles/databaseFiles.module';
8 | import { ConfigModule } from '@nestjs/config';
9 | import { LocalFilesModule } from '../localFiles/localFiles.module';
10 |
11 | @Module({
12 | imports: [
13 | TypeOrmModule.forFeature([User]),
14 | DatabaseFilesModule,
15 | StripeModule,
16 | LocalFilesModule,
17 | ConfigModule,
18 | ],
19 | providers: [UsersService],
20 | exports: [UsersService],
21 | controllers: [UsersController],
22 | })
23 | export class UsersModule {}
24 |
--------------------------------------------------------------------------------
/src/utils/excludeNull.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | NestInterceptor,
4 | ExecutionContext,
5 | CallHandler,
6 | } from '@nestjs/common';
7 | import { Observable } from 'rxjs';
8 | import { map } from 'rxjs/operators';
9 | import recursivelyStripNullValues from './recursivelyStripNullValues';
10 |
11 | @Injectable()
12 | export class ExcludeNullInterceptor implements NestInterceptor {
13 | intercept(context: ExecutionContext, next: CallHandler): Observable {
14 | return next.handle().pipe(map(value => recursivelyStripNullValues(value)));
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/findOneParams.ts:
--------------------------------------------------------------------------------
1 | import { IsNumberString } from 'class-validator';
2 |
3 | class FindOneParams {
4 | @IsNumberString()
5 | id: string;
6 | }
7 |
8 | export default FindOneParams;
9 |
--------------------------------------------------------------------------------
/src/utils/getLogLevels.ts:
--------------------------------------------------------------------------------
1 | import { LogLevel } from '@nestjs/common/services/logger.service';
2 |
3 | function getLogLevels(isProduction: boolean): LogLevel[] {
4 | if (isProduction) {
5 | return ['log', 'warn', 'error'];
6 | }
7 | return ['error', 'warn', 'log', 'verbose', 'debug'];
8 | }
9 |
10 | export default getLogLevels;
11 |
--------------------------------------------------------------------------------
/src/utils/logs.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
2 | import { Request, Response, NextFunction } from 'express';
3 |
4 | @Injectable()
5 | class LogsMiddleware implements NestMiddleware {
6 | private readonly logger = new Logger('HTTP');
7 |
8 | use(request: Request, response: Response, next: NextFunction) {
9 | response.on('finish', () => {
10 | const { method, originalUrl } = request;
11 | const { statusCode, statusMessage } = response;
12 |
13 | const message = `${method} ${originalUrl} ${statusCode} ${statusMessage}`;
14 |
15 | if (statusCode >= 500) {
16 | return this.logger.error(message);
17 | }
18 |
19 | if (statusCode >= 400) {
20 | return this.logger.warn(message);
21 | }
22 |
23 | return this.logger.log(message);
24 | });
25 |
26 | next();
27 | }
28 | }
29 |
30 | export default LogsMiddleware;
31 |
--------------------------------------------------------------------------------
/src/utils/mocks/config.service.ts:
--------------------------------------------------------------------------------
1 | const mockedConfigService = {
2 | get(key: string) {
3 | switch (key) {
4 | case 'JWT_ACCESS_TOKEN_EXPIRATION_TIME':
5 | return '3600';
6 | }
7 | },
8 | };
9 |
10 | export default mockedConfigService;
11 |
--------------------------------------------------------------------------------
/src/utils/mocks/jwt.service.ts:
--------------------------------------------------------------------------------
1 | const mockedJwtService = {
2 | sign: () => '',
3 | };
4 |
5 | export default mockedJwtService;
6 |
--------------------------------------------------------------------------------
/src/utils/rawBody.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Response } from 'express';
2 | import { json } from 'body-parser';
3 | import RequestWithRawBody from '../stripeWebhook/requestWithRawBody.interface';
4 |
5 | function rawBodyMiddleware() {
6 | return json({
7 | verify: (
8 | request: RequestWithRawBody,
9 | response: Response,
10 | buffer: Buffer,
11 | ) => {
12 | if (request.url === '/webhook' && Buffer.isBuffer(buffer)) {
13 | request.rawBody = Buffer.from(buffer);
14 | }
15 | return true;
16 | },
17 | });
18 | }
19 |
20 | export default rawBodyMiddleware;
21 |
--------------------------------------------------------------------------------
/src/utils/recursivelyStripNullValues.ts:
--------------------------------------------------------------------------------
1 | function recursivelyStripNullValues(value: unknown): unknown {
2 | if (Array.isArray(value)) {
3 | return value.map(recursivelyStripNullValues);
4 | }
5 | if (value !== null && typeof value === 'object') {
6 | return Object.fromEntries(
7 | Object.entries(value).map(([key, value]) => [
8 | key,
9 | recursivelyStripNullValues(value),
10 | ]),
11 | );
12 | }
13 | if (value !== null) {
14 | return value;
15 | }
16 | }
17 |
18 | export default recursivelyStripNullValues;
19 |
--------------------------------------------------------------------------------
/src/utils/runInCluster.ts:
--------------------------------------------------------------------------------
1 | import * as cluster from 'cluster';
2 | import * as os from 'os';
3 |
4 | export function runInCluster(bootstrap: () => Promise) {
5 | const numberOfCores = os.cpus().length;
6 |
7 | if (cluster.isMaster) {
8 | for (let i = 0; i < numberOfCores; i++) {
9 | cluster.fork();
10 | }
11 | } else {
12 | bootstrap();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/utils/scalars/timestamp.scalar.ts:
--------------------------------------------------------------------------------
1 | import { Scalar, CustomScalar } from '@nestjs/graphql';
2 | import { Kind, ValueNode } from 'graphql';
3 |
4 | @Scalar('Timestamp', () => Date)
5 | export class Timestamp implements CustomScalar {
6 | description =
7 | '`Date` type as integer. Type represents date and time as number of milliseconds from start of UNIX epoch.';
8 |
9 | serialize(value: Date) {
10 | return value instanceof Date ? value.getTime() : null;
11 | }
12 |
13 | parseValue(value: string | number | null) {
14 | try {
15 | const number = Number(value);
16 | return value !== null ? new Date(number) : null;
17 | } catch {
18 | return null;
19 | }
20 | }
21 |
22 | parseLiteral(valueNode: ValueNode) {
23 | if (valueNode.kind === Kind.INT || valueNode.kind === Kind.STRING) {
24 | try {
25 | const number = Number(valueNode.value);
26 | return new Date(number);
27 | } catch {
28 | return null;
29 | }
30 | }
31 | return null;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/stripeError.enum.ts:
--------------------------------------------------------------------------------
1 | enum StripeError {
2 | InvalidRequest = 'StripeInvalidRequestError',
3 | ResourceMissing = 'resource_missing',
4 | }
5 |
6 | export default StripeError;
7 |
--------------------------------------------------------------------------------
/src/utils/types/cacheManagerRedisStore.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'cache-manager-redis-store' {
2 | import { CacheStoreFactory } from '@nestjs/common/cache/interfaces/cache-manager.interface';
3 |
4 | const cacheStore: CacheStoreFactory;
5 |
6 | export = cacheStore;
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/types/objectWithId.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNumber } from 'class-validator';
2 |
3 | class ObjectWithIdDto {
4 | @IsNumber()
5 | id: number;
6 | }
7 |
8 | export default ObjectWithIdDto;
9 |
--------------------------------------------------------------------------------
/src/utils/types/paginationParams.ts:
--------------------------------------------------------------------------------
1 | import { IsNumber, Min, IsOptional } from 'class-validator';
2 | import { Type } from 'class-transformer';
3 |
4 | export class PaginationParams {
5 | @IsOptional()
6 | @Type(() => Number)
7 | @IsNumber()
8 | @Min(1)
9 | startId?: number;
10 |
11 | @IsOptional()
12 | @Type(() => Number)
13 | @IsNumber()
14 | @Min(0)
15 | offset?: number;
16 |
17 | @IsOptional()
18 | @Type(() => Number)
19 | @IsNumber()
20 | @Min(1)
21 | limit?: number;
22 | }
23 |
--------------------------------------------------------------------------------
/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import * as request from 'supertest';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from './../src/app.module';
4 | import { INestApplication } from '@nestjs/common';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeAll(async () => {
10 | const moduleFixture = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/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", "dist", "test", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "target": "es2019",
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "baseUrl": "./",
12 | "incremental": true,
13 | "alwaysStrict": true,
14 | "noImplicitAny": true,
15 | "allowSyntheticDefaultImports": true,
16 | "skipLibCheck": true
17 | },
18 | "exclude": ["node_modules", "dist"]
19 | }
20 |
--------------------------------------------------------------------------------