├── .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 | 2 | 3 | 4 | 5 | 6 | documentation 7 | 0% 8 | 9 | 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=""+e+"";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 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_CategoriesModule 15 | 16 | 17 | 18 | cluster_CategoriesModule_providers 19 | 20 | 21 | 22 | 23 | CategoriesService 24 | 25 | CategoriesService 26 | 27 | 28 | 29 | CategoriesModule 30 | 31 | CategoriesModule 32 | 33 | 34 | 35 | CategoriesService->CategoriesModule 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /documentation/modules/ChargeModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_ChargeModule 15 | 16 | 17 | 18 | cluster_ChargeModule_imports 19 | 20 | 21 | 22 | 23 | StripeModule 24 | 25 | StripeModule 26 | 27 | 28 | 29 | ChargeModule 30 | 31 | ChargeModule 32 | 33 | 34 | 35 | StripeModule->ChargeModule 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /documentation/modules/ChatModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_ChatModule 15 | 16 | 17 | 18 | cluster_ChatModule_imports 19 | 20 | 21 | 22 | cluster_ChatModule_providers 23 | 24 | 25 | 26 | 27 | AuthenticationModule 28 | 29 | AuthenticationModule 30 | 31 | 32 | 33 | ChatModule 34 | 35 | ChatModule 36 | 37 | 38 | 39 | AuthenticationModule->ChatModule 40 | 41 | 42 | 43 | 44 | 45 | ChatService 46 | 47 | ChatService 48 | 49 | 50 | 51 | ChatService->ChatModule 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /documentation/modules/CreditCardsModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_CreditCardsModule 15 | 16 | 17 | 18 | cluster_CreditCardsModule_imports 19 | 20 | 21 | 22 | 23 | StripeModule 24 | 25 | StripeModule 26 | 27 | 28 | 29 | CreditCardsModule 30 | 31 | CreditCardsModule 32 | 33 | 34 | 35 | StripeModule->CreditCardsModule 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /documentation/modules/EmailModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_EmailModule 15 | 16 | 17 | 18 | cluster_EmailModule_providers 19 | 20 | 21 | 22 | cluster_EmailModule_exports 23 | 24 | 25 | 26 | 27 | EmailService 28 | 29 | EmailService 30 | 31 | 32 | 33 | EmailModule 34 | 35 | EmailModule 36 | 37 | 38 | 39 | EmailModule->EmailService 40 | 41 | 42 | 43 | 44 | 45 | EmailService 46 | 47 | EmailService 48 | 49 | 50 | 51 | EmailService->EmailModule 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /documentation/modules/FilesModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_FilesModule 15 | 16 | 17 | 18 | cluster_FilesModule_providers 19 | 20 | 21 | 22 | cluster_FilesModule_exports 23 | 24 | 25 | 26 | 27 | FilesService 28 | 29 | FilesService 30 | 31 | 32 | 33 | FilesModule 34 | 35 | FilesModule 36 | 37 | 38 | 39 | FilesModule->FilesService 40 | 41 | 42 | 43 | 44 | 45 | FilesService 46 | 47 | FilesService 48 | 49 | 50 | 51 | FilesService->FilesModule 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /documentation/modules/ProductCategoriesModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_ProductCategoriesModule 15 | 16 | 17 | 18 | cluster_ProductCategoriesModule_providers 19 | 20 | 21 | 22 | 23 | ProductCategoriesService 24 | 25 | ProductCategoriesService 26 | 27 | 28 | 29 | ProductCategoriesModule 30 | 31 | ProductCategoriesModule 32 | 33 | 34 | 35 | ProductCategoriesService->ProductCategoriesModule 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /documentation/modules/ProductsModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_ProductsModule 15 | 16 | 17 | 18 | cluster_ProductsModule_providers 19 | 20 | 21 | 22 | 23 | ProductsService 24 | 25 | ProductsService 26 | 27 | 28 | 29 | ProductsModule 30 | 31 | ProductsModule 32 | 33 | 34 | 35 | ProductsService->ProductsModule 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /documentation/modules/StripeModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_StripeModule 15 | 16 | 17 | 18 | cluster_StripeModule_providers 19 | 20 | 21 | 22 | cluster_StripeModule_exports 23 | 24 | 25 | 26 | 27 | StripeService 28 | 29 | StripeService 30 | 31 | 32 | 33 | StripeModule 34 | 35 | StripeModule 36 | 37 | 38 | 39 | StripeModule->StripeService 40 | 41 | 42 | 43 | 44 | 45 | StripeService 46 | 47 | StripeService 48 | 49 | 50 | 51 | StripeService->StripeModule 52 | 53 | 54 | 55 | 56 | 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 | --------------------------------------------------------------------------------