├── .env.example ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md ├── PULL_REQUEST_TEMPLATE.md ├── labeler.yml └── workflows │ ├── codeql-analysis.yml │ ├── continuous-integration.yml │ ├── greetings.yml │ ├── label.yml │ └── stale.yml ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── config └── README.md ├── doc ├── otasoft-api-logo.png └── otasoft-core-new-architecture.png ├── docker-compose.yml ├── nest-cli.json ├── nginx ├── dev │ └── nginx.conf └── prod │ └── nginx.conf ├── package.json ├── redis └── redis.conf ├── scripts ├── README.md └── generate-ssl-cert.sh ├── src ├── app.module.ts ├── cache │ ├── README.md │ ├── config │ │ ├── cache-config.service.ts │ │ └── index.ts │ └── redis-cache.module.ts ├── doc │ ├── README.md │ ├── index.ts │ └── swagger-options.ts ├── filters │ ├── README.md │ ├── enums │ │ ├── error-code.enum.ts │ │ └── index.ts │ ├── error.filter.ts │ ├── helpers │ │ ├── index.ts │ │ └── validate-server-error.ts │ ├── index.ts │ └── interfaces │ │ ├── error-object.interface.ts │ │ └── index.ts ├── graphql │ ├── README.md │ ├── config │ │ ├── gql-config.service.ts │ │ └── index.ts │ ├── graphql-wrapper.module.ts │ ├── helpers │ │ ├── generate-gql-schema.ts │ │ └── index.ts │ └── schema.gql ├── health │ ├── README.md │ ├── controllers │ │ ├── health.controller.spec.ts │ │ ├── health.controller.ts │ │ └── index.ts │ ├── health.module.ts │ └── services │ │ ├── health.service.spec.ts │ │ ├── health.service.ts │ │ └── index.ts ├── interceptors │ ├── README.md │ ├── exclude-null.interceptor.ts │ ├── helpers │ │ ├── index.ts │ │ └── recursivelyStripNullValues.ts │ ├── index.ts │ └── timeout.interceptor.ts ├── main.ts ├── microservices │ ├── README.md │ ├── auth │ │ ├── auth.module.ts │ │ ├── graphql │ │ │ ├── decorators │ │ │ │ ├── gql-current-user.decorator.ts │ │ │ │ └── index.ts │ │ │ ├── guards │ │ │ │ ├── gql-jwt-auth.guard.ts │ │ │ │ ├── gql-jwt-refresh.guard.ts │ │ │ │ └── index.ts │ │ │ ├── input │ │ │ │ ├── auth-credentials.input.ts │ │ │ │ ├── auth-email.input.ts │ │ │ │ ├── change-password.input.ts │ │ │ │ ├── index.ts │ │ │ │ └── set-new-password.input.ts │ │ │ ├── models │ │ │ │ ├── auth-change-response-gql.model.ts │ │ │ │ ├── auth-response-status-gql.model.ts │ │ │ │ ├── auth-user-gql.model.ts │ │ │ │ ├── auth-user-id-gql.model.ts │ │ │ │ ├── auth-user-token-gql.model.ts │ │ │ │ ├── index.ts │ │ │ │ └── user.model-gql.ts │ │ │ ├── mutations │ │ │ │ ├── auth-mutation.resolver.ts │ │ │ │ ├── index.ts │ │ │ │ └── user-mutation.resolver.ts │ │ │ └── queries │ │ │ │ ├── auth-query.resolver.ts │ │ │ │ └── index.ts │ │ ├── guards │ │ │ ├── access-control.guard.ts │ │ │ └── index.ts │ │ ├── interfaces │ │ │ ├── access-control.interface.ts │ │ │ ├── index.ts │ │ │ └── token-payload.interface.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── user.model.ts │ │ ├── rest │ │ │ ├── controllers │ │ │ │ ├── auth │ │ │ │ │ ├── auth.controller.spec.ts │ │ │ │ │ └── auth.controller.ts │ │ │ │ ├── index.ts │ │ │ │ └── user │ │ │ │ │ ├── user.controller.spec.ts │ │ │ │ │ └── user.controller.ts │ │ │ ├── decorators │ │ │ │ ├── index.ts │ │ │ │ ├── rest-csrf-token.decorator.ts │ │ │ │ └── rest-current-user.decorator.ts │ │ │ ├── dto │ │ │ │ ├── auth-credentials.dto.ts │ │ │ │ ├── auth-email.dto.ts │ │ │ │ ├── change-password.dto.ts │ │ │ │ ├── get-refresh-user.dto.ts │ │ │ │ ├── index.ts │ │ │ │ └── set-new-password.dto.ts │ │ │ ├── guards │ │ │ │ ├── index.ts │ │ │ │ ├── rest-jwt-auth.guard.ts │ │ │ │ └── rest-jwt-refresh.guard.ts │ │ │ ├── interfaces │ │ │ │ ├── index.ts │ │ │ │ └── request-with-user.interface.ts │ │ │ └── models │ │ │ │ ├── auth-change-response-rest.model.ts │ │ │ │ ├── auth-user-cookie-rest.model.ts │ │ │ │ ├── auth-user-id-rest.model.ts │ │ │ │ ├── auth-user-rest.model.ts │ │ │ │ └── index.ts │ │ ├── services │ │ │ ├── auth │ │ │ │ ├── auth.service.spec.ts │ │ │ │ └── auth.service.ts │ │ │ ├── index.ts │ │ │ └── user │ │ │ │ ├── user.service.spec.ts │ │ │ │ └── user.service.ts │ │ └── strategies │ │ │ ├── index.ts │ │ │ ├── jwt-refresh-token.strategy.ts │ │ │ └── jwt.strategy.ts │ ├── booking │ │ ├── booking.module.ts │ │ ├── graphql │ │ │ ├── input │ │ │ │ ├── create-booking.input.ts │ │ │ │ └── index.ts │ │ │ ├── models │ │ │ │ ├── booking-gql.model.ts │ │ │ │ └── index.ts │ │ │ ├── mutations │ │ │ │ ├── booking-mutation.resolver.ts │ │ │ │ └── index.ts │ │ │ └── queries │ │ │ │ ├── booking-query.resolver.ts │ │ │ │ └── index.ts │ │ ├── rest │ │ │ ├── controllers │ │ │ │ ├── booking.controller.spec.ts │ │ │ │ ├── booking.controller.ts │ │ │ │ └── index.ts │ │ │ ├── dto │ │ │ │ ├── create-booking.dto.ts │ │ │ │ └── index.ts │ │ │ └── models │ │ │ │ ├── booking-rest.ts │ │ │ │ └── index.ts │ │ └── services │ │ │ ├── booking.service.spec.ts │ │ │ ├── booking.service.ts │ │ │ └── index.ts │ ├── catalog │ │ ├── catalog.module.ts │ │ ├── graphql │ │ │ ├── input │ │ │ │ ├── create-offer.input.ts │ │ │ │ ├── index.ts │ │ │ │ └── update-offer.input.ts │ │ │ ├── models │ │ │ │ ├── gql-offer.model.ts │ │ │ │ ├── gql-text-response.model.ts │ │ │ │ └── index.ts │ │ │ ├── mutations │ │ │ │ ├── index.ts │ │ │ │ └── offer-mutation.resolver.ts │ │ │ └── queries │ │ │ │ ├── index.ts │ │ │ │ └── offer-query.resolver.ts │ │ ├── interfaces │ │ │ ├── index.ts │ │ │ └── update-offer.interface.ts │ │ ├── rest │ │ │ ├── controllers │ │ │ │ ├── index.ts │ │ │ │ ├── offer.controller.spec.ts │ │ │ │ └── offer.controller.ts │ │ │ ├── dto │ │ │ │ ├── create-offer.dto.ts │ │ │ │ ├── index.ts │ │ │ │ └── update-offer.dto.ts │ │ │ └── models │ │ │ │ ├── index.ts │ │ │ │ ├── rest-offer.model.ts │ │ │ │ └── rest-text-response.model.ts │ │ └── services │ │ │ ├── index.ts │ │ │ ├── offer.service.spec.ts │ │ │ └── offer.service.ts │ ├── customer │ │ ├── customer.module.ts │ │ ├── graphql │ │ │ ├── input │ │ │ │ ├── create-customer-profile.input.ts │ │ │ │ ├── index.ts │ │ │ │ └── update-customer-profile.input.ts │ │ │ ├── models │ │ │ │ ├── customer-gql.model.ts │ │ │ │ └── index.ts │ │ │ ├── mutations │ │ │ │ ├── customer-mutation.resolver.ts │ │ │ │ └── index.ts │ │ │ └── queries │ │ │ │ ├── customer-query.resolver.ts │ │ │ │ └── index.ts │ │ ├── interfaces │ │ │ ├── index.ts │ │ │ └── update-customer-object.interface.ts │ │ ├── rest │ │ │ ├── controllers │ │ │ │ ├── customer.controller.spec.ts │ │ │ │ ├── customer.controller.ts │ │ │ │ └── index.ts │ │ │ ├── dto │ │ │ │ ├── create-customer-profile.dto.ts │ │ │ │ ├── index.ts │ │ │ │ └── update-customer-profile.dto.ts │ │ │ └── models │ │ │ │ ├── customer-rest.model.ts │ │ │ │ └── index.ts │ │ └── services │ │ │ ├── customer.service.spec.ts │ │ │ ├── customer.service.ts │ │ │ └── index.ts │ ├── index.ts │ ├── mail │ │ ├── mail.module.ts │ │ └── sendgrid │ │ │ ├── dto │ │ │ ├── index.ts │ │ │ └── send-email.dto.ts │ │ │ ├── interfaces │ │ │ └── mail-object.interface.ts │ │ │ ├── models │ │ │ ├── index.ts │ │ │ └── success-response.model.ts │ │ │ ├── sendgrid.module.ts │ │ │ └── services │ │ │ ├── sendgrid.service.spec.ts │ │ │ └── sendgrid.service.ts │ └── payment │ │ ├── graphql │ │ ├── input │ │ │ ├── create-payment.input.ts │ │ │ └── index.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── payment-gql.model.ts │ │ ├── mutations │ │ │ ├── index.ts │ │ │ └── payment-mutation.resolver.ts │ │ └── queries │ │ │ ├── index.ts │ │ │ └── payment-query.resolver.ts │ │ ├── payment.module.ts │ │ ├── rest │ │ ├── controllers │ │ │ ├── index.ts │ │ │ ├── payment.controller.spec.ts │ │ │ └── payment.controller.ts │ │ ├── dto │ │ │ ├── create-payment.dto.ts │ │ │ └── index.ts │ │ └── models │ │ │ ├── index.ts │ │ │ └── payment-rest.ts │ │ └── services │ │ ├── index.ts │ │ ├── payment.service.spec.ts │ │ └── payment.service.ts ├── open-id │ ├── README.md │ ├── controllers │ │ ├── index.ts │ │ ├── open-id.controller.spec.ts │ │ └── open-id.controller.ts │ ├── guards │ │ ├── index.ts │ │ └── open-id.guard.ts │ ├── helpers │ │ ├── build-open-id-client.ts │ │ └── index.ts │ ├── open-id.module.ts │ ├── services │ │ ├── index.ts │ │ ├── open-id.service.spec.ts │ │ └── open-id.service.ts │ └── strategies │ │ ├── index.ts │ │ ├── open-id-strategy-factory.ts │ │ └── open-id.strategy.ts ├── queues │ ├── README.md │ ├── bull-queue.module.ts │ ├── configs │ │ ├── bull-async-config.ts │ │ ├── index.ts │ │ └── queue-async-config.ts │ └── services │ │ ├── bull-queue.service.ts │ │ └── index.ts ├── security │ ├── README.md │ ├── configs │ │ ├── csurfConfigOptions.ts │ │ ├── index.ts │ │ ├── rateLimitConfig.ts │ │ └── redisSessionConfig.ts │ ├── guards │ │ ├── frontend-cookie.guard.spec.ts │ │ ├── frontend-cookie.guard.ts │ │ └── index.ts │ ├── middlewares │ │ ├── csrf.middleware.ts │ │ └── index.ts │ └── serializers │ │ ├── index.ts │ │ └── session.serializer.ts └── utils │ ├── README.md │ ├── axios │ ├── axios-wrapper.module.ts │ └── config │ │ ├── axios-async-config.ts │ │ └── index.ts │ ├── client │ ├── client.service.ts │ ├── config │ │ ├── client-async-options.ts │ │ └── index.ts │ ├── index.ts │ └── interfaces │ │ ├── index.ts │ │ └── message-pattern.interface.ts │ └── utils.module.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── webpack-hmr.config.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # Common 2 | ENVIRONMENT=development 3 | CORE_URL=https://localhost:3000/api 4 | 5 | # RabbitMQ 6 | RABBITMQ_ERLANG_COOKIE=6025e2612b6fa83647466c6a81c0cea0 7 | RABBITMQ_DEFAULT_USER=rabbitmq 8 | RABBITMQ_DEFAULT_PASS=rabbitmq 9 | RABBITMQ_DEFAULT_VHOST=otasoft-api 10 | RABBITMQ_NODENAME=localhost 11 | RABBITMQ_FIRST_HOST_PORT=5673 12 | RABBITMQ_SECOND_HOST_PORT=15673 13 | 14 | # Redis cache 15 | REDIS_HOST=localhost 16 | REDIS_PORT=6379 17 | REDIS_PASSWORD=sOmE_sEcUrE_pAsS 18 | CACHE_TTL_IN_SECONDS=5 19 | CACHE_MAX_ITEMS_IN_CACHE=10 20 | 21 | # Bull queue 22 | BULL_PREFIX=Bull 23 | BULL_MAX_JOBS=100 24 | BULL_MAX_DURATION_FOR_JOB_IN_MILISECONDS=5000 25 | QUEUE_NAME=otasoft 26 | 27 | # Security 28 | SERVE_LOCAL_SSL=self-signed 29 | FRONTEND_COOKIE=frontendCookie27 30 | SESSION_SECRET=super+secret+session+key 31 | COOKIE_SECRET=super+secret+cookie+secret 32 | OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER=https://accounts.google.com 33 | OAUTH2_CLIENT_REGISTRATION_LOGIN_CLIENT_ID=315697373972-9k2n7nnm0jg7ojoe42ch2g3qnr0ugbcq.apps.googleusercontent.com 34 | OAUTH2_CLIENT_REGISTRATION_LOGIN_CLIENT_SECRET=bYRTE2jzSadXpmBZDz3GeQbS 35 | OAUTH2_CLIENT_REGISTRATION_LOGIN_SCOPE=openid profile 36 | OAUTH2_CLIENT_REGISTRATION_LOGIN_REDIRECT_URI=https://localhost:3000/api/oidc/callback 37 | OAUTH2_CLIENT_REGISTRATION_LOGIN_POST_LOGOUT_REDIRECT_URI=https://localhost:3001 38 | JWT_ACCESS_TOKEN_SECRET=top-secret-access 39 | JWT_ACCESS_TOKEN_EXPIRATION_TIME=240s 40 | JWT_REFRESH_TOKEN_SECRET=top-secret-refresh 41 | JWT_REFRESH_TOKEN_EXPIRATION_TIME=200s 42 | 43 | # Proxy 44 | NGINX_PORT=8881 45 | 46 | # Axios 47 | AXIOS_TIMEOUT=5000 48 | AXIOS_MAX_REDIRECTS=5 -------------------------------------------------------------------------------- /.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/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: "If something isn't working as expected \U0001F914." 4 | title: '' 5 | labels: 'type: potential issue :broken_heart:,needs triage' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug Report 11 | 12 | ## Current behavior 13 | 14 | 15 | ## Input Code 16 | 17 | 18 | ```ts 19 | const your = (code) => here; 20 | ``` 21 | 22 | ## Expected behavior 23 | 24 | 25 | ## Possible Solution 26 | 27 | 28 | ## Environment 29 | 30 |

31 | Nest version: X.Y.Z
32 | 
33 |  
34 | For Tooling issues:
35 | - Node version: XX  
36 | - Platform:  
37 | 
38 | Others:
39 | 
40 | 
41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature Request" 3 | about: "I have a suggestion \U0001F63B!" 4 | title: '' 5 | labels: 'type: enhancement :wolf:,needs triage' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Feature Request 11 | 12 | ## Is your feature request related to a problem? Please describe. 13 | 14 | 15 | ## Describe the solution you'd like 16 | 17 | 18 | ## Teachability, Documentation, Adoption, Migration Strategy 19 | 20 | 21 | ## What is the motivation / use case for changing the behavior? 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Type 2 | What kind of change does this PR introduce? 3 | 4 | 5 | ``` 6 | [ ] Bugfix 7 | [ ] Feature 8 | [ ] Code style update (formatting, local variables) 9 | [ ] Refactoring (no functional changes, no api changes) 10 | [ ] Build related changes 11 | [ ] CI related changes 12 | [ ] Other... Please describe: 13 | ``` 14 | 15 | ## What is the current behavior? 16 | 17 | 18 | 19 | ## What is the new behavior? 20 | 21 | 22 | 23 | 24 | 25 | ## Other information -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add 'repo' label to any root file changes 2 | repo: 3 | - ./* 4 | 5 | # Add 'test' label to any change to *.spec.js files within the source dir and test folder 6 | test: 7 | - src/**/*.spec.js 8 | - test/**/*.spec.js 9 | 10 | # Add 'source' label to any change to src files within the source dir EXCEPT for the microservices sub-folder 11 | source: 12 | - any: ['src/**/*', '!src/microservices/*'] 13 | 14 | # Add 'microservices' label to any change to src/microservices files 15 | microservices: 16 | - src/microservices/**/* 17 | 18 | # Add 'ci' label to any change to continuos integration files inside .github folder 19 | ci: 20 | - .github/**/* -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '35 12 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: Get yarn cache directory path 28 | id: yarn-cache-dir-path 29 | run: echo "::set-output name=dir::$(yarn cache dir)" 30 | - name: Cache yarn cache 31 | uses: actions/cache@v2 32 | id: cache-yarn-cache 33 | with: 34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-yarn- 38 | - name: Cache node_modules 39 | id: cache-node-modules 40 | uses: actions/cache@v2 41 | with: 42 | path: node_modules 43 | key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }} 44 | restore-keys: | 45 | ${{ runner.os }}-${{ matrix.node-version }}-nodemodules- 46 | - name: Install dependencies 47 | run: yarn --frozen-lockfile 48 | if: | 49 | steps.cache-yarn-cache.outputs.cache-hit != 'true' || 50 | steps.cache-node-modules.outputs.cache-hit != 'true' 51 | - name: Run prettier 52 | run: yarn format 53 | - name: Run tests 54 | run: yarn test 55 | - name: Build project 56 | run: yarn build 57 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Dear Contributor! Thank you for creating an issue. We will check it shortly and get back to you with feedback. Welcome to the Otasoft Community! ;)' 13 | pr-message: 'Dear Contributor! Thank you for creating a Pull Request. We will check it shortly and get back to you with feedback. Welcome to the Otasoft Community! ;)' 14 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | # This workflow will triage pull requests and apply a label based on the 2 | # paths that are modified in the pull request. 3 | # 4 | # To use this workflow, you will need to set up a .github/labeler.yml 5 | # file with configuration. For more information, see: 6 | # https://github.com/actions/labeler 7 | 8 | name: Labeler 9 | on: [pull_request] 10 | 11 | jobs: 12 | label: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/labeler@master 18 | with: 19 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 20 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v3 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'Stale issue message' 17 | stale-pr-message: 'Stale pull request message' 18 | stale-issue-label: 'no-issue-activity' 19 | stale-pr-label: 'no-pr-activity' 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | .env 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | # Compodoc documentation 38 | /src/doc/documentation 39 | 40 | # Public Certificate and Private Key 41 | /config/private-key.key 42 | /config/public-cert.crt 43 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Otasoft API 2 | 3 | We would love for you to contribute to Otasoft API and help make it even better than it is 4 | today! As a contributor, here are the guidelines we would like you to follow: 5 | 6 | - [Issues and Bugs](#issue) 7 | - [Feature Requests](#feature) 8 | - [Coding Rules](#rules) 9 | 10 | ## Found a Bug? 11 | 12 | If you find a bug in the source code, you can help us by submitting an issue. Even better, you can submit a Pull Request with a fix. 13 | 14 | ## Missing a Feature? 15 | 16 | You can request a new feature by submitting an issue to our GitHub 17 | Repository. If you would like to implement a new feature, please submit an issue with 18 | a proposal for your work first, to be sure that we can use it. 19 | Please consider what kind of change it is: 20 | 21 | - For a **Major Feature**, first open an issue and outline your proposal so that it can be 22 | discussed. This will also allow us to better coordinate our efforts, prevent duplication of work, 23 | and help you to craft the change so that it is successfully accepted into the project. 24 | - **Small Features** can be crafted and directly submitted as a Pull Request. 25 | 26 | ## Coding Rules 27 | ### Commit Message Format 28 | ``` 29 | doc: update change readme file 30 | fix: fix problem with project build 31 | ``` 32 | 33 | ### Type 34 | 35 | Must be one of the following: 36 | 37 | - **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) 38 | - **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) 39 | - **doc**: Documentation only changes 40 | - **feat**: A new feature 41 | - **fix**: A bug fix 42 | - **refactor**: A code change that neither fixes a bug nor adds a feature 43 | - **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 44 | - **test**: Adding missing tests or correcting existing tests 45 | 46 | The subject contains succinct description of the change: 47 | 48 | - use the imperative, present tense: "change" not "changed" nor "changes" 49 | - don't capitalize first letter 50 | - no dot (.) at the end 51 | 52 | 53 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as BUILD_IMAGE 2 | 3 | RUN apk update && apk add yarn curl bash make && rm -rf /var/cache/apk/* 4 | 5 | WORKDIR /usr/share/api-gateway/otasoft-api 6 | 7 | RUN curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | bash -s -- -b /usr/local/bin 8 | 9 | COPY package.json yarn.lock ./ 10 | 11 | RUN yarn --frozen-lockfile 12 | 13 | COPY . . 14 | 15 | RUN yarn run build 16 | 17 | RUN npm prune --production 18 | 19 | RUN /usr/local/bin/node-prune 20 | 21 | FROM node:12-alpine 22 | 23 | WORKDIR /usr/share/api-gateway/otasoft-api 24 | 25 | COPY --from=BUILD_IMAGE /usr/share/api-gateway/otasoft-api/dist ./dist 26 | COPY --from=BUILD_IMAGE /usr/share/api-gateway/otasoft-api/node_modules ./node_modules 27 | 28 | EXPOSE 60320 29 | 30 | CMD ["node", "dist/main"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Otasoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | Otasoft Logo 5 | 6 | 7 |

Otasoft API - Booking engine for Online Travel Agencies

8 | 9 |

10 | 11 | 12 | 13 | Report Bug 14 | · 15 | Request Feature 16 |

17 |

18 | CI 19 |

20 | 21 | ## About The Project 22 | Otasoft Core is a Nest.js based booking engine for Online Travel Agencies (OTA's). Thanks to the microservice architecture, business logic is separated into different services allowing for greater scallability, extensibility, and separation of concerns. Otasoft Core can be configured in many ways to suit your business needs: 23 | 24 | * Databases, message brokers, authentication methods, and many more can all be configured to connect to the new or existing infrasctructure 25 | * Works great with both REST and GraphQL 26 | * Each microservice is a separate project(repo) that allows distributed development teams to work seamlessly 27 | * Modules are separate entities, so you can choose which services you would like to use in your system 28 | * Connect any modern frontend application. By default, we have Nuxt (Vue.js) and Next.js (React.js) frontends already implemented and ready to use. 29 | 30 | Otasoft projects are and always will be open source (MIT Licence). Anyone can use and support the project. The project is currently in the development phase. 31 | 32 | 33 | ## Table of Contents 34 | 35 | * [Getting Started](#getting-started) 36 | * [Documentation](#documentation) 37 | * [Architecture](#architecture) 38 | * [Layers](#layers) 39 | * [Core Team](#core-team) 40 | * [Roadmap](#roadmap) 41 | * [Contributing](#contributing) 42 | * [How to support?](#how-to-support?) 43 | * [License](#license) 44 | 45 | 46 | ## Getting Started 47 | 48 | To start developing the project please check if you have these tools installed on your machine: 49 | 50 | * [Node.js](https://nodejs.org/en/download/) 51 | * [Yarn](https://yarnpkg.com/getting-started/install) 52 | * [Docker](https://www.docker.com/get-started) 53 | 54 | Installation 55 | 56 | 1. Clone the repo 57 | ```sh 58 | git clone https://github.com/otasoft/otasoft-core 59 | ``` 60 | 2. Install all projects dependencies 61 | ```sh 62 | sh scripts/install.sh 63 | ``` 64 | 3. Copy .env.example file as .env and fill it with your environment variables 65 | ```sh 66 | cp .env.example .env 67 | ``` 68 | 4. Run docker-compose for all projects or for each individual project 69 | ```sh 70 | docker-compose up 71 | ``` 72 | 5. Run project 73 | ```sh 74 | yarn start:dev 75 | ``` 76 | 77 | When running graphql playground remember to add `"request.credentials": "same-origin"` line to your playground settings. This way, you will be able to use cookie based authentication in GQL playground. 78 | 79 | 80 | ## Documentation 81 | 82 | To generate REST Swagger documentation just run the following script: 83 | 84 | ``` 85 | yarn docs 86 | ``` 87 | 88 | To generate GraphQL schema and docs just run the project with: 89 | ``` 90 | yarn start:dev 91 | ``` 92 | 93 | And go to: 94 | ``` 95 | http://localhost:3000/graphql 96 | 97 | or 98 | 99 | https://api.otasoft.org/graphql -> If you have Nginx Reverse Proxy running and HTTPS certificate 100 | ``` 101 | 102 | ## Architecture 103 | 104 | The Otasoft API acts as a gateway/proxy for the different microservices it exposes. The GraphQL resolvers and REST controllers make calls to the RabbitMQ microservices through client-server communication. All elements of the Otasoft Core system are packed into docker images and can be run as containers. 105 | 106 | ![Architecture Diagram](doc/otasoft-core-new-architecture.png) 107 | 108 | This architecture implements the following Microservice Design Patterns: 109 | 110 | 1. [Microservice Architecture](https://microservices.io/patterns/microservices.html) 111 | 2. [Subdomain Decomposition](https://microservices.io/patterns/decomposition/decompose-by-subdomain.html) 112 | 3. [Externalized Configuration](https://microservices.io/patterns/externalized-configuration.html) 113 | 4. [Remote Procedure Invocation](https://microservices.io/patterns/communication-style/rpi.html) 114 | 5. [API Gateway](https://microservices.io/patterns/apigateway.html) 115 | 6. [Database per Service](https://microservices.io/patterns/data/database-per-service.html) 116 | 7. [CQRS](https://microservices.io/patterns/data/cqrs.html) 117 | 118 | ## Layers 119 | 120 | ### API Layer 121 | 122 | Otasoft API built using [NestJS](https://nestjs.com/) acts as the API Layer for the architecture. It takes care of listening for client requests and calling the appropriate back-end microservice to fulfill them. 123 | 124 | ### Microservice Layer 125 | 126 | [NestJS + RabbitMQ](https://www.rabbitmq.com/) was chosen as the framework for the creation of the microservices. Each service has its own database and thanks to that, microservices can work independently. All microservices are closed for any connection except the one that is coming from API Gateway. 127 | 128 | ### Persistence Layer 129 | 130 | PostgreSQL and MySQL are used as the databases and [TypeOrm](https://typeorm.io/) is used as the Object-Relational Mapper (ORM). 131 | 132 | 133 | ## Core Team 134 | 135 | Founder -> [Jakub Andrzejewski](https://www.linkedin.com/in/jakub-andrzejewski/) 136 | 137 | 138 | ## Roadmap 139 | 140 | See the [open issues](https://github.com/otasoft/otasoft-core/issues) for a list of proposed features (and known issues). 141 | 142 | 143 | ## Contributing 144 | 145 | You are welcome to contribute to Otasoft projects. Please see [contribution tips](CONTRIBUTING.md) 146 | 147 | 148 | ## How to support? 149 | Otasoft projects are and always will be Open Source. 150 | 151 | Core team and contributors in the Otasoft ecosystem spend their free and off work time to make this project grow. If you would like to support us you can do so by: 152 | 153 | - contributing - it does not matter whether it is writing code, creating designs, or sharing knowledge in our e-books and pdfs. Any help is always welcome! 154 | - evangelizing - share a good news about Otasoft projects in social media or during technology conferences ;) 155 | 156 | 157 | ## License 158 | 159 | Distributed under the [MIT licensed](LICENSE). See `LICENSE` for more information. -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # CONFIG 2 | 3 | This directory contains config and secrets like private key and public certificate generated with OpenSSL for HTTPS protocol. 4 | 5 | More information about the usage of OpenSSL in [the documentation](https://www.openssl.org/). 6 | -------------------------------------------------------------------------------- /doc/otasoft-api-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otasoft/otasoft-api/9073c678affb55399b27858c75fd0bbf84028cbc/doc/otasoft-api-logo.png -------------------------------------------------------------------------------- /doc/otasoft-core-new-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otasoft/otasoft-api/9073c678affb55399b27858c75fd0bbf84028cbc/doc/otasoft-core-new-architecture.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | 4 | rabbitmq: 5 | container_name: rabbitmq 6 | image: rabbitmq:3.8.8-management-alpine 7 | hostname: otasoft-api-rabbitmq 8 | ports: 9 | - ${RABBITMQ_FIRST_HOST_PORT}:5672 10 | - ${RABBITMQ_SECOND_HOST_PORT}:15672 11 | volumes: 12 | - ./data/rabbitmq:/var/lib/rabbitmq/mnesia/rabbit@app-rabbitmq:cached 13 | env_file: 14 | - .env 15 | networks: 16 | - otasoft-api-network 17 | 18 | redis: 19 | container_name: redis 20 | image: redis:5-alpine 21 | command: redis-server --requirepass ${REDIS_PASSWORD} 22 | volumes: 23 | - $PWD/redis/redis.conf:/usr/local/etc/redis/redis.conf 24 | environment: 25 | - REDIS_REPLICATION_MODE=master 26 | ports: 27 | - ${REDIS_PORT}:6379 28 | networks: 29 | - otasoft-api-network 30 | 31 | nginx: 32 | image: nginx:alpine 33 | container_name: nginx--dev 34 | ports: 35 | - 80:80 36 | - 443:443 37 | volumes: 38 | - ${PWD}/nginx/dev:/etc/nginx/conf.d/ 39 | - ./config/public-cert.crt:/etc/ssl/public-cert.crt 40 | - ./config/private-key.key:/etc/ssl/private-key.key 41 | ulimits: 42 | nproc: 65535 43 | networks: 44 | - otasoft-api-network 45 | 46 | networks: 47 | otasoft-api-network: 48 | driver: bridge -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "plugins": ["@nestjs/swagger/plugin"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /nginx/dev/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream otasoft-api { 2 | server docker.for.mac.host.internal:3000; # Device localhost inside Docker for Mac 3 | # server host.docker.internal:3000 # Device localhost inside Docker for Windows 4 | } 5 | 6 | server { 7 | listen 80 default_server; 8 | listen [::]:80 default_server; 9 | return 301 https://$server_name$request_uri; 10 | } 11 | 12 | server { 13 | listen 80; 14 | listen 443 ssl http2 default_server; 15 | listen [::]:443 ssl http2 default_server; 16 | keepalive_timeout 70; 17 | server_name localhost; 18 | ssl_session_cache shared:SSR:10m; 19 | ssl_session_timeout 10m; 20 | ssl_certificate /etc/ssl/public-cert.crt; 21 | ssl_certificate_key /etc/ssl/private-key.key; 22 | 23 | access_log /var/log/nginx/nginx.access.log; 24 | error_log /var/log/nginx/nginx.error.log; 25 | 26 | location / { 27 | proxy_pass http://otasoft-api; 28 | proxy_http_version 1.1; 29 | proxy_set_header Upgrade $http_upgrade; 30 | proxy_set_header Connection 'upgrade'; 31 | proxy_set_header Host $host; 32 | proxy_cache_bypass $http_upgrade; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nginx/prod/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream otasoft-api { 2 | server otasoft-api:3000; 3 | } 4 | 5 | server { 6 | listen 80 default_server; 7 | listen [::]:80 default_server; 8 | server_name localhost; 9 | return 301 https://$server_name$request_uri; 10 | } 11 | 12 | server { 13 | listen 80; 14 | listen 443 ssl http2 default_server; 15 | listen [::]:443 ssl http2 default_server; 16 | keepalive_timeout 70; 17 | server_name localhost; 18 | ssl_session_cache shared:SSR:10m; 19 | ssl_session_timeout 10m; 20 | ssl_certificate /etc/ssl/public-cert.crt; 21 | ssl_certificate_key /etc/ssl/private-key.key; 22 | 23 | access_log /var/log/nginx/nginx.access.log; 24 | error_log /var/log/nginx/nginx.error.log; 25 | 26 | location / { 27 | proxy_pass http://otasoft-api; 28 | # proxy_redirect off; 29 | proxy_http_version 1.1; 30 | proxy_set_header Upgrade $http_upgrade; 31 | proxy_set_header Connection 'upgrade'; 32 | proxy_set_header Host $host; 33 | proxy_cache_bypass $http_upgrade; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "otasoft-api", 3 | "version": "0.2.8", 4 | "description": "API Gateway for Otasoft ecosystem", 5 | "author": "Jakub Andrzejewski", 6 | "license": "MIT", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:dev-hmr": "nest build --webpack --webpackPath webpack-hmr.config.js", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json", 22 | "docs": "npx compodoc --theme material --output src/doc/documentation -p tsconfig.json -s" 23 | }, 24 | "dependencies": { 25 | "@nestjs/bull": "^0.3.1", 26 | "@nestjs/common": "^7.6.11", 27 | "@nestjs/config": "^0.6.3", 28 | "@nestjs/core": "^7.6.11", 29 | "@nestjs/graphql": "^7.9.8", 30 | "@nestjs/jwt": "^7.2.0", 31 | "@nestjs/microservices": "^7.6.11", 32 | "@nestjs/passport": "^7.1.4", 33 | "@nestjs/platform-express": "^7.6.11", 34 | "@nestjs/schedule": "^0.4.2", 35 | "@nestjs/swagger": "^4.7.12", 36 | "@nestjs/terminus": "^7.1.0", 37 | "amqp-connection-manager": "^3.2.0", 38 | "amqplib": "^0.6.0", 39 | "apollo-server-express": "^2.19.2", 40 | "bull": "^3.20.1", 41 | "cache-manager": "^3.4.0", 42 | "cache-manager-redis-store": "^2.0.0", 43 | "class-transformer": "^0.3.2", 44 | "class-validator": "^0.13.0", 45 | "compression": "^1.7.4", 46 | "connect-redis": "^5.0.0", 47 | "cookie-parser": "^1.4.5", 48 | "csurf": "^1.11.0", 49 | "express-rate-limit": "^5.2.3", 50 | "express-session": "^1.17.1", 51 | "graphql": "^15.5.0", 52 | "graphql-tools": "^7.0.2", 53 | "helmet": "^4.4.1", 54 | "module-alias": "^2.2.2", 55 | "openid-client": "^4.4.0", 56 | "passport": "^0.4.1", 57 | "passport-jwt": "^4.0.0", 58 | "passport-local": "^1.0.0", 59 | "redis": "^3.0.2", 60 | "reflect-metadata": "^0.1.13", 61 | "rimraf": "^3.0.2", 62 | "rxjs": "^6.6.3", 63 | "swagger-ui-express": "^4.1.6" 64 | }, 65 | "devDependencies": { 66 | "@compodoc/compodoc": "^1.1.11", 67 | "@nestjs/cli": "^7.5.4", 68 | "@nestjs/schematics": "^7.2.7", 69 | "@nestjs/testing": "^7.6.11", 70 | "@types/bull": "^3.15.0", 71 | "@types/cookie-parser": "^1.4.2", 72 | "@types/csurf": "^1.11.0", 73 | "@types/express": "^4.17.11", 74 | "@types/jest": "26.0.20", 75 | "@types/node": "^14.14.25", 76 | "@types/supertest": "^2.0.8", 77 | "@typescript-eslint/eslint-plugin": "^4.14.2", 78 | "@typescript-eslint/parser": "^4.14.2", 79 | "eslint": "7.19.0", 80 | "eslint-config-prettier": "^7.2.0", 81 | "eslint-plugin-import": "^2.20.1", 82 | "jest": "26.6.3", 83 | "prettier": "^2.2.1", 84 | "supertest": "^6.1.3", 85 | "ts-jest": "26.5.0", 86 | "ts-loader": "^8.0.15", 87 | "ts-node": "9.1.1", 88 | "tsconfig-paths": "^3.9.0", 89 | "typescript": "^4.1.3", 90 | "webpack-node-externals": "^2.5.2" 91 | }, 92 | "jest": { 93 | "moduleFileExtensions": [ 94 | "js", 95 | "json", 96 | "ts" 97 | ], 98 | "rootDir": "src", 99 | "testRegex": ".spec.ts$", 100 | "transform": { 101 | "^.+\\.(t|j)s$": "ts-jest" 102 | }, 103 | "coverageDirectory": "../coverage", 104 | "testEnvironment": "node", 105 | "moduleNameMapper": { 106 | "@auth/(.*)": "/microservices/auth/$1", 107 | "@booking/(.*)": "/microservices/booking/$1", 108 | "@catalog/(.*)": "/microservices/catalog/$1", 109 | "@customer/(.*)": "/microservices/customer/$1", 110 | "@mail/(.*)": "/microservices/mail/$1", 111 | "@utils/(.*)": "/utils/$1", 112 | "@test/(.*)": "/../test/$1", 113 | "@security/(.*)": "/security/$1" 114 | } 115 | }, 116 | "_moduleAliases": { 117 | "@infrastructure": "dist/item/infrastructure", 118 | "@auth": "dist/microservices/auth", 119 | "@booking": "dist/microservices/booking", 120 | "@catalog": "dist/microservices/catalog", 121 | "@customer": "dist/microservices/customer", 122 | "@mail": "dist/microservices/mail", 123 | "@utils": "dist/utils", 124 | "@security": "dist/security" 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /redis/redis.conf: -------------------------------------------------------------------------------- 1 | protected-mode yes 2 | port 6379 -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # SCRIPTS 2 | 3 | This directory contains utility scripts. 4 | -------------------------------------------------------------------------------- /scripts/generate-ssl-cert.sh: -------------------------------------------------------------------------------- 1 | openssl req -x509 -newkey rsa:4096 -sha256 -keyout ./config/private-key.key -out ./config/public-cert.crt -days 365 -subj "/C=CA/ST=QC/O=Company, Inc./CN=api.otasoft.org" -nodes 2 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { ScheduleModule } from '@nestjs/schedule'; 4 | 5 | import { GraphqlWrapperModule } from './graphql/graphql-wrapper.module'; 6 | import { RedisCacheModule } from './cache/redis-cache.module'; 7 | import { HealthModule } from './health/health.module'; 8 | import { MicroservicesModules } from './microservices'; 9 | import { BullQueueModule } from './queues/bull-queue.module'; 10 | import { UtilsModule } from './utils/utils.module'; 11 | import { OpenIdModule } from './open-id/open-id.module'; 12 | 13 | @Module({ 14 | imports: [ 15 | ConfigModule.forRoot({ isGlobal: true }), 16 | ScheduleModule.forRoot(), 17 | GraphqlWrapperModule, 18 | RedisCacheModule, 19 | BullQueueModule, 20 | HealthModule, 21 | UtilsModule, 22 | OpenIdModule, 23 | ...MicroservicesModules, 24 | ], 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /src/cache/README.md: -------------------------------------------------------------------------------- 1 | # Cache 2 | 3 | Redis Cache Module. 4 | 5 | This directory contains: 6 | 7 | - RedisCacheModule which is a wrapper for CacheModule 8 | - `config` directory with CacheConfigService and index.ts exporting that service 9 | - schema.gql 10 | -------------------------------------------------------------------------------- /src/cache/config/cache-config.service.ts: -------------------------------------------------------------------------------- 1 | import { CacheModuleAsyncOptions, CacheOptionsFactory } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import * as redisStore from 'cache-manager-redis-store'; 4 | 5 | export class CacheConfigService implements CacheOptionsFactory { 6 | createCacheOptions(): CacheModuleAsyncOptions { 7 | return { 8 | imports: [ConfigModule], 9 | inject: [ConfigService], 10 | useFactory: (configService: ConfigService) => ({ 11 | store: redisStore, 12 | host: configService.get('REDIS_HOST'), 13 | port: configService.get('REDIS_PORT'), 14 | ttl: configService.get('CACHE_TTL_IN_SECONDS'), 15 | max: configService.get('CACHE_MAX_ITEMS_IN_CACHE'), 16 | }), 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cache/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cache-config.service'; 2 | -------------------------------------------------------------------------------- /src/cache/redis-cache.module.ts: -------------------------------------------------------------------------------- 1 | import { CacheModule, Global, Module } from '@nestjs/common'; 2 | 3 | import { CacheConfigService } from './config'; 4 | 5 | @Global() 6 | @Module({ 7 | imports: [CacheModule.registerAsync({ useClass: CacheConfigService })], 8 | exports: [CacheModule], 9 | }) 10 | export class RedisCacheModule {} 11 | -------------------------------------------------------------------------------- /src/doc/README.md: -------------------------------------------------------------------------------- 1 | # Doc 2 | 3 | Documentation folder for Swagger options 4 | 5 | This directory contains: 6 | 7 | - swagger-options object used to generate swagger documentation 8 | - index.ts exporting that file 9 | -------------------------------------------------------------------------------- /src/doc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './swagger-options'; 2 | -------------------------------------------------------------------------------- /src/doc/swagger-options.ts: -------------------------------------------------------------------------------- 1 | import { DocumentBuilder } from '@nestjs/swagger'; 2 | 3 | export const swaggerOptions = new DocumentBuilder() 4 | .setTitle('Otasoft API') 5 | .setDescription( 6 | 'An API for microservice booking engine for Online Travel Agencies', 7 | ) 8 | .setVersion(process.env.npm_package_version) 9 | .build(); 10 | -------------------------------------------------------------------------------- /src/filters/README.md: -------------------------------------------------------------------------------- 1 | # Filters 2 | 3 | Global Nest.js filters that can be used across the application. Currently implemented: 4 | 5 | - ErrorFilter -> used for validating server errors in the application 6 | 7 | This directory contains: 8 | 9 | - ErrorFilter 10 | - `enums` directory with error-code enum for storing specific error codes and index.ts exporting that file 11 | - `helpers` directory with validate-server-error method and index.ts exporting that file 12 | - `interfaces` directory with error-object interface and index.ts exporting that file 13 | - index.ts exporting filers 14 | -------------------------------------------------------------------------------- /src/filters/enums/error-code.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorCodeEnum { 2 | InvalidCsrfToken = 'EBADCSRFTOKEN', 3 | } 4 | -------------------------------------------------------------------------------- /src/filters/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error-code.enum'; 2 | -------------------------------------------------------------------------------- /src/filters/error.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | HttpException, 5 | ArgumentsHost, 6 | HttpStatus, 7 | } from '@nestjs/common'; 8 | 9 | import { validateServerError } from './helpers'; 10 | 11 | /** 12 | * A Nest.js filter that catches errors throws appriopriate exceptions 13 | */ 14 | @Catch() 15 | export class ErrorFilter implements ExceptionFilter { 16 | catch(error, host: ArgumentsHost) { 17 | let request = host.switchToHttp().getRequest(); 18 | let response = host.switchToHttp().getResponse(); 19 | let status = 20 | error instanceof HttpException 21 | ? error.getStatus() 22 | : HttpStatus.INTERNAL_SERVER_ERROR; 23 | 24 | let statusCode; 25 | let errorResponse; 26 | 27 | if (status === HttpStatus.INTERNAL_SERVER_ERROR) { 28 | const { code, message } = validateServerError(error.code); 29 | 30 | statusCode = code; 31 | errorResponse = message; 32 | } else { 33 | statusCode = status; 34 | errorResponse = error.getResponse(); 35 | } 36 | 37 | return response.status(status).json({ 38 | statusCode, 39 | errorResponse, 40 | timestamp: new Date().toISOString(), 41 | path: request.url, 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/filters/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './validate-server-error'; 2 | -------------------------------------------------------------------------------- /src/filters/helpers/validate-server-error.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCodeEnum } from '../enums'; 2 | import { IErrorObject } from '../interfaces'; 3 | 4 | /** 5 | * A method that returns a correct error object based on the error code provided as a parameter 6 | * 7 | * @param {string | number} errorCode 8 | * @return {*} {IErrorObject} 9 | */ 10 | export const validateServerError = ( 11 | errorCode: string | number, 12 | ): IErrorObject => { 13 | switch (errorCode) { 14 | case ErrorCodeEnum.InvalidCsrfToken: 15 | return { 16 | code: 403, 17 | message: 'Invalid CSRF token', 18 | }; 19 | default: 20 | return { 21 | code: 500, 22 | message: 'Internal Server Error', 23 | }; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error.filter'; 2 | -------------------------------------------------------------------------------- /src/filters/interfaces/error-object.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IErrorObject { 2 | code: number; 3 | message: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/filters/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error-object.interface'; 2 | -------------------------------------------------------------------------------- /src/graphql/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL 2 | 3 | GraphQL Wrapper Module. 4 | 5 | This directory contains: 6 | 7 | - GraphqlWrapperModule which is a wrapper for GraphQLModule 8 | - `config` directory with GqlConfigService and index.ts exporting that service 9 | - `helpers` directory with generate-graphql-schema method to manually trigger schema generation 10 | - schema.gql 11 | -------------------------------------------------------------------------------- /src/graphql/config/gql-config.service.ts: -------------------------------------------------------------------------------- 1 | import { GqlModuleOptions, GqlOptionsFactory } from '@nestjs/graphql'; 2 | import { join } from 'path'; 3 | 4 | import { MicroservicesModules } from '../../microservices'; 5 | 6 | export class GqlConfigService implements GqlOptionsFactory { 7 | createGqlOptions(): GqlModuleOptions { 8 | return { 9 | include: [...MicroservicesModules], 10 | autoSchemaFile: join(process.cwd(), 'src/graphql/schema.gql'), 11 | context: ({ req, res }) => ({ req, res }), 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/graphql/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gql-config.service'; 2 | -------------------------------------------------------------------------------- /src/graphql/graphql-wrapper.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GraphQLModule } from '@nestjs/graphql'; 3 | 4 | import { GqlConfigService } from './config'; 5 | 6 | @Module({ 7 | imports: [GraphQLModule.forRootAsync({ useClass: GqlConfigService })], 8 | }) 9 | export class GraphqlWrapperModule {} 10 | -------------------------------------------------------------------------------- /src/graphql/helpers/generate-gql-schema.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { 3 | GraphQLSchemaBuilderModule, 4 | GraphQLSchemaFactory, 5 | } from '@nestjs/graphql'; 6 | import { printSchema } from 'graphql'; 7 | 8 | import { MicroservicesModules } from '../../microservices'; 9 | 10 | async function generateGqlSchema() { 11 | const app = await NestFactory.create(GraphQLSchemaBuilderModule); 12 | await app.init(); 13 | 14 | const gqlSchemaFactory = app.get(GraphQLSchemaFactory); 15 | const schema = await gqlSchemaFactory.create(MicroservicesModules); 16 | 17 | console.log(printSchema(schema)); 18 | } 19 | -------------------------------------------------------------------------------- /src/graphql/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generate-gql-schema'; 2 | -------------------------------------------------------------------------------- /src/graphql/schema.gql: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------ 2 | # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 3 | # ------------------------------------------------------ 4 | 5 | type GqlAuthChangeResponse { 6 | response: String! 7 | } 8 | 9 | type GqlAuthUser { 10 | auth_id: ID! 11 | token: String! 12 | } 13 | 14 | type GqlAuthUserId { 15 | auth_id: ID! 16 | } 17 | 18 | type GqlAuthResponseStatus { 19 | status: String! 20 | } 21 | 22 | type GqlUserModel { 23 | id: Float! 24 | email: String! 25 | } 26 | 27 | type GqlBooking { 28 | id: ID! 29 | customer_id: Float! 30 | } 31 | 32 | type GqlOfferModel { 33 | activity_id: ID! 34 | name: String! 35 | description: String! 36 | } 37 | 38 | type GqlTextResponseModel { 39 | response: String! 40 | } 41 | 42 | type GqlCustomer { 43 | id: ID! 44 | first_name: String! 45 | last_name: String! 46 | } 47 | 48 | type GqlPayment { 49 | id: ID! 50 | booking_id: Float! 51 | date: DateTime! 52 | } 53 | 54 | """ 55 | A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. 56 | """ 57 | scalar DateTime 58 | 59 | type Query { 60 | getUserId(email: AuthEmailInput!): GqlAuthUserId! 61 | confirmAccountCreation(token: String!): Boolean! 62 | authenticate: GqlUserModel! 63 | getAuthenticatedUser(authCredentialsInput: AuthCredentialsInput!): GqlAuthUser! 64 | getBooking(id: Int!): GqlBooking! 65 | getSingleOffer(id: Float!): GqlOfferModel! 66 | getAllOffers: GqlOfferModel! 67 | getOffersByQuery(query: String!): GqlOfferModel! 68 | getCustomerProfile(id: Int!): GqlCustomer! 69 | getPayment(id: Int!): GqlPayment! 70 | } 71 | 72 | input AuthEmailInput { 73 | email: String! 74 | } 75 | 76 | input AuthCredentialsInput { 77 | email: String! 78 | password: String! 79 | } 80 | 81 | type Mutation { 82 | signUp(authCredentials: AuthCredentialsInput!): GqlAuthUser! 83 | signIn(authCredentials: AuthCredentialsInput!): GqlUserModel! 84 | signOut: GqlAuthResponseStatus! 85 | refresh: GqlUserModel! 86 | changeUserPassword(changePasswordInput: ChangePasswordInput!, id: Float!): GqlAuthChangeResponse! 87 | deleteUserAccount(id: Float!): GqlAuthChangeResponse! 88 | forgotPassword(email: AuthEmailInput!): GqlAuthChangeResponse! 89 | setNewPassword(setNewPasswordInput: SetNewPasswordInput!, token: String!): GqlAuthChangeResponse! 90 | createBooking(createBookingData: CreateBookingInput!): GqlBooking! 91 | deleteBooking(id: Float!): Boolean! 92 | updateBooking(updateBookingData: CreateBookingInput!, id: Float!): GqlBooking! 93 | createOffer(createOfferInput: CreateOfferInput!): GqlOfferModel! 94 | updateOffer(updateOfferInput: UpdateOfferInput!, id: Float!): GqlOfferModel! 95 | deleteOffer(id: Float!): GqlTextResponseModel! 96 | createCustomerProfile(createCustomerProfileData: CreateCustomerProfileInput!): GqlCustomer! 97 | removeCustomerProfile(id: Float!): Boolean! 98 | updateCustomerProfile(updateCustomerProfileData: UpdateCustomerProfileInput!, id: Float!): GqlCustomer! 99 | createPayment(createPaymentData: CreatePaymentInput!): GqlPayment! 100 | updatePayment(updatePaymentData: CreatePaymentInput!, id: Float!): GqlPayment! 101 | } 102 | 103 | input ChangePasswordInput { 104 | old_password: String! 105 | new_password: String! 106 | } 107 | 108 | input SetNewPasswordInput { 109 | new_password: String! 110 | } 111 | 112 | input CreateBookingInput { 113 | customer_id: Float! 114 | } 115 | 116 | input CreateOfferInput { 117 | name: String! 118 | description: String! 119 | } 120 | 121 | input UpdateOfferInput { 122 | name: String! 123 | description: String! 124 | } 125 | 126 | input CreateCustomerProfileInput { 127 | first_name: String! 128 | last_name: String! 129 | } 130 | 131 | input UpdateCustomerProfileInput { 132 | first_name: String! 133 | last_name: String! 134 | } 135 | 136 | input CreatePaymentInput { 137 | booking_id: Float! 138 | amount: Float! 139 | card_token: Float! 140 | } 141 | -------------------------------------------------------------------------------- /src/health/README.md: -------------------------------------------------------------------------------- 1 | # Health 2 | 3 | Health checks module. Health checks are used to check the state of the microservice. Currently implemented: 4 | 5 | - Health check (returns plain text string idicating that API Gateway is working correctly) 6 | - Ping check 7 | - Storage check 8 | - Heap and RSS checks 9 | - Microservice check (with microservice name provided as parameter, i.e `auth`) 10 | 11 | This directory contains: 12 | 13 | - HealthModule which is a wrapper for TerminusModule 14 | - `services` directory with HealthService and index.ts exporting that service 15 | - `controllers` directory with HealthController and index.ts exporting that controller 16 | -------------------------------------------------------------------------------- /src/health/controllers/health.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { TerminusModule } from '@nestjs/terminus'; 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | 5 | import { HealthController } from './health.controller'; 6 | import { HealthService } from '../services'; 7 | 8 | describe('HealthController', () => { 9 | let controller: HealthController; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | imports: [TerminusModule], 14 | controllers: [HealthController], 15 | providers: [HealthService, ConfigService], 16 | }).compile(); 17 | 18 | controller = module.get(HealthController); 19 | }); 20 | 21 | it('should be defined', () => { 22 | expect(controller).toBeDefined(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/health/controllers/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param } from '@nestjs/common'; 2 | import { HealthCheck } from '@nestjs/terminus'; 3 | 4 | import { HealthService } from '../services'; 5 | 6 | @Controller('health') 7 | export class HealthController { 8 | constructor(private readonly healthService: HealthService) {} 9 | 10 | @Get() 11 | checkHealth(): string { 12 | return this.healthService.checkHealth(); 13 | } 14 | 15 | @Get('/check-ping') 16 | @HealthCheck() 17 | checkPing() { 18 | return this.healthService.checkPing(); 19 | } 20 | 21 | @Get('/check-disk') 22 | @HealthCheck() 23 | checkDisk() { 24 | return this.healthService.checkDisk(); 25 | } 26 | 27 | @Get('/check-memory') 28 | @HealthCheck() 29 | checkMemory() { 30 | return this.healthService.checkMemory(); 31 | } 32 | 33 | @Get('/check-microservice/:name') 34 | @HealthCheck() 35 | checkMicroservice(@Param('name') microserviceName: string) { 36 | return this.healthService.checkMicroservice(microserviceName); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/health/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './health.controller'; 2 | -------------------------------------------------------------------------------- /src/health/health.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TerminusModule } from '@nestjs/terminus'; 3 | 4 | import { HealthController } from './controllers'; 5 | import { HealthService } from './services'; 6 | 7 | @Module({ 8 | imports: [TerminusModule], 9 | controllers: [HealthController], 10 | providers: [HealthService], 11 | }) 12 | export class HealthModule {} 13 | -------------------------------------------------------------------------------- /src/health/services/health.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { TerminusModule } from '@nestjs/terminus'; 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | 5 | import { HealthService } from './health.service'; 6 | 7 | describe('HealthService', () => { 8 | let service: HealthService; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | imports: [TerminusModule], 13 | providers: [HealthService, ConfigService], 14 | }).compile(); 15 | 16 | service = module.get(HealthService); 17 | }); 18 | 19 | it('should be defined', () => { 20 | expect(service).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/health/services/health.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { Transport } from '@nestjs/microservices'; 4 | import { Cron, CronExpression } from '@nestjs/schedule'; 5 | import { 6 | DiskHealthIndicator, 7 | HealthCheckService, 8 | HttpHealthIndicator, 9 | MemoryHealthIndicator, 10 | MicroserviceHealthIndicator, 11 | } from '@nestjs/terminus'; 12 | 13 | @Injectable() 14 | export class HealthService { 15 | constructor( 16 | private readonly healthCheckService: HealthCheckService, 17 | private readonly httpHealthIndicator: HttpHealthIndicator, 18 | private readonly diskHealthIndicator: DiskHealthIndicator, 19 | private readonly memoryHealthIndicator: MemoryHealthIndicator, 20 | private readonly microserviceHealthIndicator: MicroserviceHealthIndicator, 21 | private readonly configService: ConfigService, 22 | ) {} 23 | 24 | checkHealth(): string { 25 | return 'API Gateway is working correctly'; 26 | } 27 | 28 | checkPing() { 29 | return this.healthCheckService.check([ 30 | () => 31 | this.httpHealthIndicator.pingCheck( 32 | 'otasoft-api', 33 | this.configService.get('CORE_URL'), 34 | { timeout: 3000, proxy: { host: '127.0.0.1', port: 443 } }, 35 | ), 36 | ]); 37 | } 38 | 39 | @Cron(CronExpression.EVERY_30_MINUTES) 40 | checkDisk() { 41 | return this.healthCheckService.check([ 42 | () => 43 | this.diskHealthIndicator.checkStorage('otasoft-api', { 44 | thresholdPercent: 0.9, 45 | path: '/', 46 | }), 47 | ]); 48 | } 49 | 50 | checkMemory() { 51 | return this.healthCheckService.check([ 52 | () => 53 | this.memoryHealthIndicator.checkHeap('memory_heap', 150 * 1024 * 1024), 54 | () => 55 | this.memoryHealthIndicator.checkRSS('memory_rss', 150 * 1024 * 1024), 56 | ]); 57 | } 58 | 59 | checkMicroservice(microserviceName: string) { 60 | return this.healthCheckService.check([ 61 | () => 62 | this.microserviceHealthIndicator.pingCheck( 63 | `rabbitmq-${microserviceName}`, 64 | { 65 | transport: Transport.RMQ, 66 | options: { 67 | urls: [ 68 | `amqp://${process.env.RABBITMQ_DEFAULT_USER}:${process.env.RABBITMQ_DEFAULT_PASS}@${process.env.RABBITMQ_NODENAME}:${process.env.RABBITMQ_FIRST_HOST_PORT}/${process.env.RABBITMQ_DEFAULT_VHOST}`, 69 | ], 70 | queue: `${microserviceName}_queue`, 71 | queueOptions: { 72 | durable: false, 73 | }, 74 | }, 75 | }, 76 | ), 77 | ]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/health/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './health.service'; 2 | -------------------------------------------------------------------------------- /src/interceptors/README.md: -------------------------------------------------------------------------------- 1 | # Interceptors 2 | 3 | Global Nest.js interceptors that can be used across the application. Currently implemented: 4 | 5 | - TimeoutInterceptor -> used for droping requests that reach certain timeout 6 | - ExcludeNullInterceptor -> used for excluding null values provided as parameters 7 | 8 | This directory contains: 9 | 10 | - TimeoutInterceptor 11 | - ExcludeNullInterceptor 12 | - `helpers` directory with recursivelyStripNullValues method 13 | - index.ts exporting all interceptors 14 | -------------------------------------------------------------------------------- /src/interceptors/exclude-null.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 | 10 | import { recursivelyStripNullValues } from './helpers'; 11 | 12 | @Injectable() 13 | export class ExcludeNullInterceptor implements NestInterceptor { 14 | intercept(context: ExecutionContext, next: CallHandler): Observable { 15 | return next 16 | .handle() 17 | .pipe(map((value) => recursivelyStripNullValues(value))); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/interceptors/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './recursivelyStripNullValues'; 2 | -------------------------------------------------------------------------------- /src/interceptors/helpers/recursivelyStripNullValues.ts: -------------------------------------------------------------------------------- 1 | export const 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 | -------------------------------------------------------------------------------- /src/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './exclude-null.interceptor'; 2 | export * from './timeout.interceptor'; 3 | -------------------------------------------------------------------------------- /src/interceptors/timeout.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestInterceptor, 4 | ExecutionContext, 5 | CallHandler, 6 | RequestTimeoutException, 7 | } from '@nestjs/common'; 8 | import { Observable, throwError, TimeoutError } from 'rxjs'; 9 | import { catchError, timeout } from 'rxjs/operators'; 10 | 11 | @Injectable() 12 | export class TimeoutInterceptor implements NestInterceptor { 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | return next.handle().pipe( 15 | timeout(5000), 16 | catchError((err) => { 17 | if (err instanceof TimeoutError) { 18 | return throwError(new RequestTimeoutException()); 19 | } 20 | return throwError(err); 21 | }), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { INestApplication, ValidationPipe } from '@nestjs/common'; 3 | import { SwaggerModule } from '@nestjs/swagger'; 4 | import * as helmet from 'helmet'; 5 | import * as rateLimit from 'express-rate-limit'; 6 | import * as compression from 'compression'; 7 | import * as cookieParser from 'cookie-parser'; 8 | import * as passport from 'passport'; 9 | import * as csurf from 'csurf'; 10 | 11 | import { AppModule } from './app.module'; 12 | import { swaggerOptions } from './doc'; 13 | import { 14 | rateLimitConfigObject, 15 | createRedisSession, 16 | csurfConfigOptions, 17 | } from './security/configs'; 18 | import { FrontendCookieGuard } from './security/guards'; 19 | import { ExcludeNullInterceptor, TimeoutInterceptor } from './interceptors'; 20 | import { csrfMiddleware } from './security/middlewares'; 21 | import { ErrorFilter } from './filters'; 22 | 23 | declare const module: any; 24 | 25 | (async function bootstrap() { 26 | const app: INestApplication = await NestFactory.create(AppModule); 27 | app.setGlobalPrefix('api'); 28 | app.useGlobalPipes(new ValidationPipe({ skipMissingProperties: true })); 29 | app.useGlobalInterceptors( 30 | new ExcludeNullInterceptor(), 31 | new TimeoutInterceptor(), 32 | ); 33 | app.useGlobalFilters(new ErrorFilter()); 34 | 35 | app.use(cookieParser(process.env.COOKIE_SECRET)); 36 | app.use(createRedisSession()); 37 | app.use(passport.initialize()); 38 | app.use(passport.session()); 39 | 40 | const csrf = csurf(csurfConfigOptions); 41 | app.use((req, res, next) => { 42 | csrfMiddleware(req, res, next, csrf); 43 | }); 44 | 45 | if (process.env.ENVIRONMENT === 'development') { 46 | const document = SwaggerModule.createDocument(app, swaggerOptions); 47 | SwaggerModule.setup('doc', app, document); 48 | app.use(compression()); 49 | } else { 50 | app.use(helmet()); 51 | app.use(rateLimit(rateLimitConfigObject)); 52 | app.useGlobalGuards(new FrontendCookieGuard()); 53 | } 54 | 55 | await app.listen(3000); 56 | 57 | if (module.hot) { 58 | module.hot.accept(); 59 | module.hot.dispose(() => app.close()); 60 | } 61 | })(); 62 | -------------------------------------------------------------------------------- /src/microservices/README.md: -------------------------------------------------------------------------------- 1 | # Microservices 2 | 3 | This directory contains: 4 | 5 | - AuthMicroservice used for handling user authentication and authorization 6 | - BookingMicroservice used for handling offer booking by certain user 7 | - CatalogMicroservice used for handling offer actions (CRUD) 8 | - CustomerMicroservice used for handling customer related stuff (customer name, lastname, age, and so on) 9 | - MailMicroservice used for handling sending automatic emails 10 | -------------------------------------------------------------------------------- /src/microservices/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ClientsModule } from '@nestjs/microservices'; 3 | import { PassportModule } from '@nestjs/passport'; 4 | import { JwtModule } from '@nestjs/jwt'; 5 | 6 | import { createClientAsyncOptions } from '@utils/client'; 7 | import { MailModule } from '@mail/mail.module'; 8 | import { AuthServices } from './services'; 9 | import { AuthControllers } from './rest/controllers'; 10 | import { AuthMutations } from './graphql/mutations'; 11 | import { AuthQueries } from './graphql/queries'; 12 | import { JwtRefreshTokenStrategy, JwtStrategy } from './strategies'; 13 | import { SessionSerializer } from '@security/serializers'; 14 | 15 | @Module({ 16 | imports: [ 17 | ClientsModule.registerAsync([createClientAsyncOptions('auth')]), 18 | PassportModule, 19 | JwtModule.register({}), 20 | MailModule, 21 | ], 22 | controllers: [...AuthControllers], 23 | providers: [ 24 | SessionSerializer, 25 | JwtStrategy, 26 | JwtRefreshTokenStrategy, 27 | ...AuthServices, 28 | ...AuthMutations, 29 | ...AuthQueries, 30 | ], 31 | }) 32 | export class AuthModule {} 33 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/decorators/gql-current-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { GqlExecutionContext } from '@nestjs/graphql'; 3 | 4 | export const GqlCurrentUser = createParamDecorator( 5 | (data: unknown, context: ExecutionContext) => { 6 | const ctx = GqlExecutionContext.create(context); 7 | return ctx.getContext().req.user; 8 | }, 9 | ); 10 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gql-current-user.decorator'; 2 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/guards/gql-jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { GqlExecutionContext } from '@nestjs/graphql'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | 5 | @Injectable() 6 | export class GqlJwtAuthGuard extends AuthGuard('jwt') { 7 | getRequest(context: ExecutionContext) { 8 | const ctx = GqlExecutionContext.create(context); 9 | return ctx.getContext().req; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/guards/gql-jwt-refresh.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { GqlExecutionContext } from '@nestjs/graphql'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | 5 | @Injectable() 6 | export class GqlJwtRefreshGuard extends AuthGuard('jwt-refresh-token') { 7 | getRequest(context: ExecutionContext) { 8 | const ctx = GqlExecutionContext.create(context); 9 | return ctx.getContext().req; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gql-jwt-auth.guard'; 2 | export * from './gql-jwt-refresh.guard'; 3 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/input/auth-credentials.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | import { 3 | IsEmail, 4 | IsString, 5 | Matches, 6 | MaxLength, 7 | MinLength, 8 | } from 'class-validator'; 9 | 10 | @InputType() 11 | export class AuthCredentialsInput { 12 | @Field() 13 | @IsString() 14 | @IsEmail() 15 | @MinLength(8) 16 | @MaxLength(30) 17 | email: string; 18 | 19 | @Field() 20 | @IsString() 21 | @MinLength(8) 22 | @MaxLength(30) 23 | @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 24 | message: 'password too weak', 25 | }) 26 | password: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/input/auth-email.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator'; 3 | 4 | @InputType() 5 | export class AuthEmailInput { 6 | @Field() 7 | @IsString() 8 | @IsEmail() 9 | @MinLength(8) 10 | @MaxLength(30) 11 | email: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/input/change-password.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | import { IsString, Matches, MaxLength, MinLength } from 'class-validator'; 3 | 4 | @InputType() 5 | export class ChangePasswordInput { 6 | @Field() 7 | @IsString() 8 | @MinLength(8) 9 | @MaxLength(30) 10 | @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 11 | message: 'password too weak', 12 | }) 13 | old_password: string; 14 | 15 | @Field() 16 | @IsString() 17 | @MinLength(8) 18 | @MaxLength(30) 19 | @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 20 | message: 'password too weak', 21 | }) 22 | new_password: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-credentials.input'; 2 | export * from './auth-email.input'; 3 | export * from './change-password.input'; 4 | export * from './set-new-password.input'; 5 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/input/set-new-password.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | import { IsString, Matches, MaxLength, MinLength } from 'class-validator'; 3 | 4 | @InputType() 5 | export class SetNewPasswordInput { 6 | @Field() 7 | @IsString() 8 | @MinLength(8) 9 | @MaxLength(30) 10 | @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 11 | message: 'password too weak', 12 | }) 13 | new_password: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/models/auth-change-response-gql.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class GqlAuthChangeResponse { 5 | @Field() 6 | response: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/models/auth-response-status-gql.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | import { IsString } from 'class-validator'; 3 | 4 | @ObjectType() 5 | export class GqlAuthResponseStatus { 6 | @Field() 7 | @IsString() 8 | status: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/models/auth-user-gql.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class GqlAuthUser { 5 | @Field((type) => ID) 6 | auth_id: number; 7 | 8 | @Field() 9 | token: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/models/auth-user-id-gql.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class GqlAuthUserId { 5 | @Field((type) => ID) 6 | auth_id: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/models/auth-user-token-gql.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | import { IsString } from 'class-validator'; 3 | 4 | @ObjectType() 5 | export class GqlAuthUserToken { 6 | @Field() 7 | @IsString() 8 | cookie: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-change-response-gql.model'; 2 | export * from './auth-user-gql.model'; 3 | export * from './auth-user-id-gql.model'; 4 | export * from './auth-user-token-gql.model'; 5 | export * from './auth-response-status-gql.model'; 6 | export * from './user.model-gql'; 7 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/models/user.model-gql.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | import { IsEmail, IsInt } from 'class-validator'; 3 | 4 | @ObjectType() 5 | export class GqlUserModel { 6 | @Field() 7 | @IsInt() 8 | id: number; 9 | 10 | @Field() 11 | @IsEmail() 12 | email: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/mutations/auth-mutation.resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassSerializerInterceptor, 3 | HttpCode, 4 | UseGuards, 5 | UseInterceptors, 6 | } from '@nestjs/common'; 7 | import { Args, Context, Mutation, Resolver } from '@nestjs/graphql'; 8 | 9 | import { GqlJwtAuthGuard, GqlJwtRefreshGuard } from '../guards'; 10 | import { UserModel } from '../../models'; 11 | import { AuthCredentialsInput } from '../input'; 12 | import { GqlCurrentUser } from '../decorators'; 13 | import { GqlAuthResponseStatus, GqlAuthUser, GqlUserModel } from '../models'; 14 | import { AuthService } from '../../services/auth/auth.service'; 15 | 16 | @UseInterceptors(ClassSerializerInterceptor) 17 | @Resolver((of) => GqlAuthUser) 18 | export class AuthMutationResolver { 19 | constructor(private readonly authService: AuthService) {} 20 | 21 | @Mutation((returns) => GqlAuthUser) 22 | async signUp( 23 | @Args('authCredentials') authCredentialsInput: AuthCredentialsInput, 24 | ): Promise { 25 | return this.authService.signUp(authCredentialsInput); 26 | } 27 | 28 | @HttpCode(200) 29 | @Mutation((returns) => GqlUserModel) 30 | async signIn( 31 | @Context() context, 32 | @Args('authCredentials') authCredentialsInput: AuthCredentialsInput, 33 | ): Promise { 34 | const response = await this.authService.signIn(authCredentialsInput); 35 | 36 | context.res.setHeader('Set-Cookie', [...response.cookies]); 37 | 38 | return response.user; 39 | } 40 | 41 | @UseGuards(GqlJwtAuthGuard) 42 | @Mutation((returns) => GqlAuthResponseStatus) 43 | async signOut( 44 | @GqlCurrentUser() user: UserModel, 45 | @Context() context: any, 46 | ): Promise { 47 | const signOutCookies = await this.authService.signOut(user.id); 48 | 49 | context.res.setHeader('Set-Cookie', [...signOutCookies]); 50 | 51 | return { status: 'Signed Out' }; 52 | } 53 | 54 | @UseGuards(GqlJwtRefreshGuard) 55 | @Mutation((returns) => GqlUserModel) 56 | async refresh(@GqlCurrentUser() user: UserModel, @Context() context: any) { 57 | const accessTokenCookie = await this.authService.getCookieWithJwtAccessToken( 58 | user.id, 59 | ); 60 | 61 | context.res.setHeader('Set-Cookie', accessTokenCookie); 62 | 63 | return user; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/mutations/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthMutationResolver } from './auth-mutation.resolver'; 2 | import { UserMutationResolver } from './user-mutation.resolver'; 3 | 4 | export const AuthMutations = [AuthMutationResolver, UserMutationResolver]; 5 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/mutations/user-mutation.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Resolver } from '@nestjs/graphql'; 3 | 4 | import { AccessControlGuard } from '../../guards/access-control.guard'; 5 | import { 6 | AuthEmailInput, 7 | ChangePasswordInput, 8 | SetNewPasswordInput, 9 | } from '../input'; 10 | import { GqlAuthChangeResponse, GqlAuthUser } from '../models'; 11 | import { UserService } from '../../services/user/user.service'; 12 | 13 | @Resolver((of) => GqlAuthUser) 14 | export class UserMutationResolver { 15 | constructor(private readonly userService: UserService) {} 16 | 17 | @UseGuards(AccessControlGuard) 18 | @Mutation((returns) => GqlAuthChangeResponse) 19 | async changeUserPassword( 20 | @Args('id') id: number, 21 | @Args('changePasswordInput') changePasswordInput: ChangePasswordInput, 22 | ): Promise { 23 | return this.userService.changeUserPassword(id, changePasswordInput); 24 | } 25 | 26 | @UseGuards(AccessControlGuard) 27 | @Mutation((returns) => GqlAuthChangeResponse) 28 | async deleteUserAccount( 29 | @Args('id') id: number, 30 | ): Promise { 31 | return this.userService.deleteUserAccount(id); 32 | } 33 | 34 | @Mutation((returns) => GqlAuthChangeResponse) 35 | async forgotPassword( 36 | @Args('email') authEmailInput: AuthEmailInput, 37 | ): Promise { 38 | return this.userService.forgotPassword(authEmailInput); 39 | } 40 | 41 | @Mutation((returns) => GqlAuthChangeResponse) 42 | async setNewPassword( 43 | @Args('token') token: string, 44 | @Args('setNewPasswordInput') setNewPasswordInput: SetNewPasswordInput, 45 | ): Promise { 46 | return this.userService.setNewPassword(token, setNewPasswordInput); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/queries/auth-query.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common'; 2 | import { Args, Resolver, Query } from '@nestjs/graphql'; 3 | 4 | import { GqlAuthUser, GqlAuthUserId, GqlUserModel } from '../models'; 5 | import { AuthCredentialsInput, AuthEmailInput } from '../input'; 6 | import { AccessControlGuard } from '../../guards'; 7 | import { GqlJwtAuthGuard } from '../guards'; 8 | import { GqlCurrentUser } from '../decorators'; 9 | import { UserService } from '../../services/user/user.service'; 10 | 11 | @Resolver((of) => GqlAuthUser) 12 | export class AuthQueryResolver { 13 | constructor(private readonly userService: UserService) {} 14 | 15 | @UseGuards(AccessControlGuard) 16 | @Query((returns) => GqlAuthUserId) 17 | async getUserId( 18 | @Args('email') authEmailInput: AuthEmailInput, 19 | ): Promise { 20 | return this.userService.getUserId(authEmailInput); 21 | } 22 | 23 | @Query((returns) => Boolean) 24 | async confirmAccountCreation(@Args('token') token: string): Promise { 25 | return this.userService.confirmAccountCreation(token); 26 | } 27 | 28 | @UseGuards(GqlJwtAuthGuard) 29 | @Query((returns) => GqlUserModel) 30 | authenticate(@GqlCurrentUser() user: GqlUserModel) { 31 | return user; 32 | } 33 | 34 | @Query((returns) => GqlAuthUser) 35 | getAuthenticatedUser( 36 | @Args('authCredentialsInput') authCredentialsInput: AuthCredentialsInput, 37 | ): Promise { 38 | return this.userService.getAuthenticatedUser(authCredentialsInput); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/microservices/auth/graphql/queries/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthQueryResolver } from './auth-query.resolver'; 2 | 3 | export const AuthQueries = [AuthQueryResolver]; 4 | -------------------------------------------------------------------------------- /src/microservices/auth/guards/access-control.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | CanActivate, 4 | ExecutionContext, 5 | Inject, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | import { ClientProxy } from '@nestjs/microservices'; 9 | import { Request } from 'express'; 10 | 11 | import { ClientService } from '@utils/client'; 12 | import { IAccessControl } from '../interfaces'; 13 | 14 | export class AccessControlGuard implements CanActivate { 15 | constructor( 16 | @Inject('AUTH_MICROSERVICE') 17 | private readonly authClient: ClientProxy, 18 | private readonly clientService: ClientService, 19 | ) {} 20 | 21 | async canActivate(context: ExecutionContext): Promise { 22 | let req: Request; 23 | req = context.getArgs()[context.getArgs().length - 2].req; 24 | if (!req) { 25 | req = context.switchToHttp().getRequest(); 26 | } 27 | 28 | const jwt: string = req.cookies['Authentication']; 29 | 30 | if (!jwt) throw new UnauthorizedException('User not authenticated'); 31 | 32 | const id: number = 33 | parseInt(req.params.id, 10) || parseInt(context.getArgs()[1].id, 10); 34 | 35 | if (!id) throw new BadRequestException('Missing user ID'); 36 | 37 | const accessControlObject: IAccessControl = { 38 | jwt, 39 | id, 40 | }; 41 | 42 | return this.clientService.sendMessageWithPayload( 43 | this.authClient, 44 | { role: 'authorization', cmd: 'checkAccess' }, 45 | accessControlObject, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/microservices/auth/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './access-control.guard'; 2 | -------------------------------------------------------------------------------- /src/microservices/auth/interfaces/access-control.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IAccessControl { 2 | jwt: string; 3 | id: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/microservices/auth/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './access-control.interface'; 2 | export * from './token-payload.interface'; 3 | -------------------------------------------------------------------------------- /src/microservices/auth/interfaces/token-payload.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ITokenPayload { 2 | userId: number; 3 | } 4 | -------------------------------------------------------------------------------- /src/microservices/auth/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.model'; 2 | -------------------------------------------------------------------------------- /src/microservices/auth/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsInt } from 'class-validator'; 2 | 3 | export class UserModel { 4 | @IsInt() 5 | id: number; 6 | 7 | @IsEmail() 8 | email: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/controllers/auth/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { AuthService } from '../../../services/auth/auth.service'; 5 | import { AuthController } from './auth.controller'; 6 | import { createClientAsyncOptions } from '../../../../../utils/client'; 7 | import { UtilsModule } from '../../../../../utils/utils.module'; 8 | import { MailModule } from '../../../../mail/mail.module'; 9 | 10 | describe('AuthController', () => { 11 | let controller: AuthController; 12 | 13 | beforeEach(async () => { 14 | const module: TestingModule = await Test.createTestingModule({ 15 | imports: [ 16 | ClientsModule.registerAsync([createClientAsyncOptions('auth')]), 17 | UtilsModule, 18 | MailModule, 19 | ], 20 | controllers: [AuthController], 21 | providers: [AuthService], 22 | }).compile(); 23 | 24 | controller = module.get(AuthController); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(controller).toBeDefined(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/controllers/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | Body, 5 | UseGuards, 6 | Get, 7 | HttpCode, 8 | Req, 9 | UseInterceptors, 10 | ClassSerializerInterceptor, 11 | } from '@nestjs/common'; 12 | 13 | import { RestJwtAuthGuard, RestJwtRefreshGuard } from '../../guards'; 14 | import { RestCurrentUser, RestCsrfToken } from '../../decorators'; 15 | import { AuthCredentialsDto } from '../../dto'; 16 | import { RestAuthChangeResponse, RestAuthUser } from '../../models'; 17 | import { IRequestWithUser } from '../../interfaces'; 18 | import { UserModel } from '../../../models'; 19 | import { AuthService } from '../../../services/auth/auth.service'; 20 | 21 | @UseInterceptors(ClassSerializerInterceptor) 22 | @Controller('auth') 23 | export class AuthController { 24 | constructor(private readonly authService: AuthService) {} 25 | 26 | @UseGuards(RestJwtAuthGuard) 27 | @Get() 28 | authenticate(@RestCurrentUser() user: UserModel, @RestCsrfToken() csrfToken) { 29 | return { 30 | user, 31 | csrfToken, 32 | }; 33 | } 34 | 35 | @Post('/signup') 36 | async signUp( 37 | @Body() authCredentialsDto: AuthCredentialsDto, 38 | ): Promise { 39 | return this.authService.signUp(authCredentialsDto); 40 | } 41 | 42 | @HttpCode(200) 43 | @Post('/signin') 44 | async signIn( 45 | @Body() authCredentialsDto: AuthCredentialsDto, 46 | @Req() request: IRequestWithUser, 47 | ): Promise { 48 | const response = await this.authService.signIn(authCredentialsDto); 49 | 50 | request.res.setHeader('Set-Cookie', [...response.cookies]); 51 | 52 | return response.user; 53 | } 54 | 55 | @UseGuards(RestJwtAuthGuard) 56 | @HttpCode(200) 57 | @Post('/signout') 58 | async signOut(@Req() req: IRequestWithUser): Promise { 59 | const signOutCookies = await this.authService.signOut(req.user.id); 60 | 61 | req.res.setHeader('Set-Cookie', [...signOutCookies]); 62 | 63 | return { response: 'Signed Out' }; 64 | } 65 | 66 | @UseGuards(RestJwtRefreshGuard) 67 | @Get('refresh') 68 | async refresh(@Req() req: IRequestWithUser): Promise { 69 | const accessTokenCookie = await this.authService.getCookieWithJwtAccessToken( 70 | req.user.id, 71 | ); 72 | 73 | req.res.setHeader('Set-Cookie', accessTokenCookie); 74 | 75 | return req.user; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthController } from './auth/auth.controller'; 2 | import { UserController } from './user/user.controller'; 3 | 4 | export const AuthControllers = [AuthController, UserController]; 5 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/controllers/user/user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { UserService } from '../../../services/user/user.service'; 5 | import { UserController } from './user.controller'; 6 | import { createClientAsyncOptions } from '../../../../../utils/client'; 7 | import { UtilsModule } from '../../../../../utils/utils.module'; 8 | import { MailModule } from '../../../../mail/mail.module'; 9 | 10 | describe('UserController', () => { 11 | let controller: UserController; 12 | 13 | beforeEach(async () => { 14 | const module: TestingModule = await Test.createTestingModule({ 15 | imports: [ 16 | ClientsModule.registerAsync([ 17 | createClientAsyncOptions('auth'), 18 | createClientAsyncOptions('customer'), 19 | createClientAsyncOptions('mail'), 20 | ]), 21 | UtilsModule, 22 | MailModule, 23 | ], 24 | controllers: [UserController], 25 | providers: [UserService], 26 | }).compile(); 27 | 28 | controller = module.get(UserController); 29 | }); 30 | 31 | it('should be defined', () => { 32 | expect(controller).toBeDefined(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/controllers/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Post, 8 | Put, 9 | Query, 10 | UseGuards, 11 | } from '@nestjs/common'; 12 | 13 | import { AccessControlGuard } from '../../../guards'; 14 | import { UserService } from '../../../services/user/user.service'; 15 | import { 16 | AuthCredentialsDto, 17 | AuthEmailDto, 18 | ChangePasswordDto, 19 | SetNewPasswordDto, 20 | } from '../../dto'; 21 | import { 22 | RestAuthChangeResponse, 23 | RestAuthUser, 24 | RestAuthUserId, 25 | } from '../../models'; 26 | 27 | @Controller('user') 28 | export class UserController { 29 | constructor(private readonly userService: UserService) {} 30 | 31 | @UseGuards(AccessControlGuard) 32 | @Get('/get-user-id') 33 | async getUserId( 34 | @Query('email') email: AuthEmailDto, 35 | ): Promise { 36 | return this.userService.getUserId(email); 37 | } 38 | 39 | @Get('/get-authenticated-user') 40 | async getAuthenticatedUser( 41 | authCredentialsDto: AuthCredentialsDto, 42 | ): Promise { 43 | return this.userService.getAuthenticatedUser(authCredentialsDto); 44 | } 45 | 46 | @UseGuards(AccessControlGuard) 47 | @Put('/:id/change-user-password') 48 | async changeUserPassword( 49 | @Param('id') id: number, 50 | @Body() changePasswordDto: ChangePasswordDto, 51 | ): Promise { 52 | return this.userService.changeUserPassword(id, changePasswordDto); 53 | } 54 | 55 | @UseGuards(AccessControlGuard) 56 | @Delete('/:id/delete-user-account') 57 | async deleteUserAccount( 58 | @Param('id') id: number, 59 | ): Promise { 60 | return this.userService.deleteUserAccount(id); 61 | } 62 | 63 | @Get('/confirm/:token') 64 | async confirmAccountCreation( 65 | @Param('token') token: string, 66 | ): Promise { 67 | return this.userService.confirmAccountCreation(token); 68 | } 69 | 70 | @Post('/forgot-password') 71 | async forgotPassword( 72 | @Body() authEmailDto: AuthEmailDto, 73 | ): Promise { 74 | return this.userService.forgotPassword(authEmailDto); 75 | } 76 | 77 | @Post('/set-new-password/:token') 78 | async setNewPassword( 79 | @Param('token') token: string, 80 | @Body() setNewPasswordDto: SetNewPasswordDto, 81 | ): Promise { 82 | return this.userService.setNewPassword(token, setNewPasswordDto); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rest-current-user.decorator'; 2 | export * from './rest-csrf-token.decorator'; 3 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/decorators/rest-csrf-token.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const RestCsrfToken = createParamDecorator( 4 | (data: unknown, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest(); 6 | return request.cookies._csrf; 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/decorators/rest-current-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const RestCurrentUser = createParamDecorator( 4 | (data: unknown, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest(); 6 | return request.user; 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/dto/auth-credentials.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsString, 3 | MinLength, 4 | MaxLength, 5 | Matches, 6 | IsEmail, 7 | } from 'class-validator'; 8 | 9 | export class AuthCredentialsDto { 10 | @MinLength(8) 11 | @MaxLength(20) 12 | @IsEmail() 13 | email: string; 14 | 15 | @IsString() 16 | @MinLength(8) 17 | @MaxLength(30) 18 | @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 19 | message: 'password too weak', 20 | }) 21 | password: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/dto/auth-email.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsString, 3 | MinLength, 4 | MaxLength, 5 | Matches, 6 | IsEmail, 7 | } from 'class-validator'; 8 | 9 | export class AuthEmailDto { 10 | @IsString() 11 | @MinLength(8) 12 | @MaxLength(40) 13 | @IsEmail() 14 | email: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/dto/change-password.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, MinLength, MaxLength, Matches } from 'class-validator'; 2 | 3 | export class ChangePasswordDto { 4 | @IsString() 5 | @MinLength(8) 6 | @MaxLength(30) 7 | @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 8 | message: 'password too weak', 9 | }) 10 | old_password: string; 11 | 12 | @IsString() 13 | @MinLength(8) 14 | @MaxLength(30) 15 | @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 16 | message: 'password too weak', 17 | }) 18 | new_password: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/dto/get-refresh-user.dto.ts: -------------------------------------------------------------------------------- 1 | export class GetRefreshUserDto { 2 | refreshToken: string; 3 | id: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-credentials.dto'; 2 | export * from './auth-email.dto'; 3 | export * from './change-password.dto'; 4 | export * from './get-refresh-user.dto'; 5 | export * from './set-new-password.dto'; 6 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/dto/set-new-password.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, MinLength, MaxLength, Matches } from 'class-validator'; 2 | 3 | export class SetNewPasswordDto { 4 | @IsString() 5 | @MinLength(8) 6 | @MaxLength(30) 7 | @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 8 | message: 'password too weak', 9 | }) 10 | new_password: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rest-jwt-auth.guard'; 2 | export * from './rest-jwt-refresh.guard'; 3 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/guards/rest-jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class RestJwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/guards/rest-jwt-refresh.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class RestJwtRefreshGuard extends AuthGuard('jwt-refresh-token') {} 6 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request-with-user.interface'; 2 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/interfaces/request-with-user.interface.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { UserModel } from '../../models'; 3 | 4 | export interface IRequestWithUser extends Request { 5 | user: UserModel; 6 | } 7 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/models/auth-change-response-rest.model.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | export class RestAuthChangeResponse { 4 | @IsString() 5 | response: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/models/auth-user-cookie-rest.model.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | export class RestAuthUserCookie { 4 | @IsString() 5 | cookie: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/models/auth-user-id-rest.model.ts: -------------------------------------------------------------------------------- 1 | import { IsInt } from 'class-validator'; 2 | 3 | export class RestAuthUserId { 4 | @IsInt() 5 | auth_id: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/models/auth-user-rest.model.ts: -------------------------------------------------------------------------------- 1 | import { IsInt, IsString } from 'class-validator'; 2 | 3 | export class RestAuthUser { 4 | @IsInt() 5 | auth_id: number; 6 | 7 | @IsString() 8 | token: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/microservices/auth/rest/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-change-response-rest.model'; 2 | export * from './auth-user-id-rest.model'; 3 | export * from './auth-user-rest.model'; 4 | export * from './auth-user-cookie-rest.model'; 5 | -------------------------------------------------------------------------------- /src/microservices/auth/services/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { AuthService } from './auth.service'; 5 | import { createClientAsyncOptions } from '../../../../utils/client'; 6 | import { UtilsModule } from '../../../../utils/utils.module'; 7 | import { MailModule } from '../../../mail/mail.module'; 8 | 9 | describe('AuthService', () => { 10 | let service: AuthService; 11 | 12 | beforeEach(async () => { 13 | const module: TestingModule = await Test.createTestingModule({ 14 | imports: [ 15 | ClientsModule.registerAsync([createClientAsyncOptions('auth')]), 16 | UtilsModule, 17 | MailModule, 18 | ], 19 | providers: [AuthService], 20 | }).compile(); 21 | 22 | service = module.get(AuthService); 23 | }); 24 | 25 | it('should be defined', () => { 26 | expect(service).toBeDefined(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/microservices/auth/services/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@nestjs/common'; 2 | import { ClientProxy } from '@nestjs/microservices'; 3 | 4 | import { AuthCredentialsInput } from '../../graphql/input'; 5 | import { GqlAuthUser } from '../../graphql/models'; 6 | import { AuthCredentialsDto } from '../../rest/dto'; 7 | import { RestAuthUser } from '../../rest/models'; 8 | import { ClientService } from '@utils/client'; 9 | import { SendgridService } from '@mail/sendgrid/services/sendgrid.service'; 10 | 11 | @Injectable() 12 | export class AuthService { 13 | constructor( 14 | @Inject('AUTH_MICROSERVICE') 15 | private readonly authClient: ClientProxy, 16 | private readonly clientService: ClientService, 17 | private readonly sendgridService: SendgridService, 18 | ) {} 19 | 20 | async signUp( 21 | authCredentialsData: AuthCredentialsDto | AuthCredentialsInput, 22 | ): Promise { 23 | const authUser: Promise< 24 | GqlAuthUser | RestAuthUser 25 | > = this.clientService.sendMessageWithPayload( 26 | this.authClient, 27 | { role: 'auth', cmd: 'register' }, 28 | authCredentialsData, 29 | ); 30 | 31 | this.sendgridService.sendConfirmCreateAccountEmail({ 32 | customer_email: authCredentialsData.email, 33 | token: (await authUser).token, 34 | }); 35 | 36 | return authUser; 37 | } 38 | 39 | async signIn(authCredentialsData: AuthCredentialsDto | AuthCredentialsInput) { 40 | return this.clientService.sendMessageWithPayload( 41 | this.authClient, 42 | { role: 'auth', cmd: 'login' }, 43 | authCredentialsData, 44 | ); 45 | } 46 | 47 | async signOut(id: number): Promise { 48 | return this.clientService.sendMessageWithPayload( 49 | this.authClient, 50 | { role: 'auth', cmd: 'logout' }, 51 | id, 52 | ); 53 | } 54 | 55 | async getCookieWithJwtAccessToken(id: number) { 56 | return this.clientService.sendMessageWithPayload( 57 | this.authClient, 58 | { role: 'authorization', cmd: 'getCookieWithJwtAccessToken' }, 59 | id, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/microservices/auth/services/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from './auth/auth.service'; 2 | import { UserService } from './user/user.service'; 3 | 4 | export const AuthServices = [AuthService, UserService]; 5 | -------------------------------------------------------------------------------- /src/microservices/auth/services/user/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { UserService } from './user.service'; 5 | import { createClientAsyncOptions } from '../../../../utils/client'; 6 | import { UtilsModule } from '../../../../utils/utils.module'; 7 | import { MailModule } from '../../../mail/mail.module'; 8 | 9 | describe('UserService', () => { 10 | let service: UserService; 11 | 12 | beforeEach(async () => { 13 | const module: TestingModule = await Test.createTestingModule({ 14 | imports: [ 15 | ClientsModule.registerAsync([ 16 | createClientAsyncOptions('auth'), 17 | createClientAsyncOptions('customer'), 18 | createClientAsyncOptions('mail'), 19 | ]), 20 | UtilsModule, 21 | MailModule, 22 | ], 23 | providers: [UserService], 24 | }).compile(); 25 | 26 | service = module.get(UserService); 27 | }); 28 | 29 | it('should be defined', () => { 30 | expect(service).toBeDefined(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/microservices/auth/services/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Inject, Injectable } from '@nestjs/common'; 2 | import { ClientProxy } from '@nestjs/microservices'; 3 | 4 | import { 5 | RestAuthChangeResponse, 6 | RestAuthUser, 7 | RestAuthUserId, 8 | } from '../../rest/models'; 9 | import { 10 | GqlAuthChangeResponse, 11 | GqlAuthUser, 12 | GqlAuthUserId, 13 | } from '../../graphql/models'; 14 | import { 15 | AuthCredentialsDto, 16 | AuthEmailDto, 17 | ChangePasswordDto, 18 | SetNewPasswordDto, 19 | GetRefreshUserDto, 20 | } from '../../rest/dto'; 21 | import { 22 | AuthCredentialsInput, 23 | AuthEmailInput, 24 | ChangePasswordInput, 25 | SetNewPasswordInput, 26 | } from '../../graphql/input'; 27 | import { ClientService } from '@utils/client'; 28 | import { SendgridService } from '@mail/sendgrid/services/sendgrid.service'; 29 | 30 | @Injectable() 31 | export class UserService { 32 | constructor( 33 | @Inject('AUTH_MICROSERVICE') 34 | private readonly authClient: ClientProxy, 35 | private readonly clientService: ClientService, 36 | private readonly sendgridService: SendgridService, 37 | ) {} 38 | 39 | async getUserId( 40 | authEmailData: AuthEmailDto | AuthEmailInput, 41 | ): Promise { 42 | return this.clientService.sendMessageWithPayload( 43 | this.authClient, 44 | { role: 'user', cmd: 'getId' }, 45 | authEmailData, 46 | ); 47 | } 48 | 49 | async getUserById(id: number) { 50 | return this.clientService.sendMessageWithPayload( 51 | this.authClient, 52 | { role: 'user', cmd: 'getUserById' }, 53 | id, 54 | ); 55 | } 56 | 57 | async getUserIfRefreshTokenMatches( 58 | getRefreshUserData: GetRefreshUserDto, 59 | ): Promise { 60 | return this.clientService.sendMessageWithPayload( 61 | this.authClient, 62 | { role: 'user', cmd: 'getUserIfRefreshTokenMatches' }, 63 | getRefreshUserData, 64 | ); 65 | } 66 | 67 | async getAuthenticatedUser( 68 | authCredentialsData: AuthCredentialsDto | AuthCredentialsInput, 69 | ): Promise { 70 | return this.clientService.sendMessageWithPayload( 71 | this.authClient, 72 | { role: 'user', cmd: 'getAuthenticatedUser' }, 73 | authCredentialsData, 74 | ); 75 | } 76 | 77 | async changeUserPassword( 78 | id: number, 79 | changePasswordData: ChangePasswordDto | ChangePasswordInput, 80 | ): Promise { 81 | if (changePasswordData.old_password === changePasswordData.new_password) 82 | throw new BadRequestException( 83 | 'New password cannot be the same as the old password', 84 | ); 85 | return this.clientService.sendMessageWithPayload( 86 | this.authClient, 87 | { role: 'user', cmd: 'changePassword' }, 88 | { id, changePasswordData }, 89 | ); 90 | } 91 | 92 | async deleteUserAccount( 93 | id: number, 94 | ): Promise { 95 | return this.clientService.sendMessageWithPayload( 96 | this.authClient, 97 | { role: 'user', cmd: 'deleteAccount' }, 98 | id, 99 | ); 100 | } 101 | 102 | async confirmAccountCreation(token: string): Promise { 103 | return this.clientService.sendMessageWithPayload( 104 | this.authClient, 105 | { role: 'user', cmd: 'confirmAccount' }, 106 | token, 107 | ); 108 | } 109 | 110 | async removeRefreshToken(userId: number): Promise { 111 | return this.clientService.sendMessageWithPayload( 112 | this.authClient, 113 | { role: 'user', cmd: 'removeRefreshToken' }, 114 | userId, 115 | ); 116 | } 117 | 118 | async forgotPassword( 119 | authEmailData: AuthEmailDto | AuthEmailInput, 120 | ): Promise { 121 | const forgotPasswordToken: Promise = this.clientService.sendMessageWithPayload( 122 | this.authClient, 123 | { role: 'user', cmd: 'forgot-password' }, 124 | authEmailData, 125 | ); 126 | 127 | const response = this.sendgridService.sendForgotPasswordEmail({ 128 | customer_email: authEmailData.email, 129 | token: await forgotPasswordToken, 130 | }); 131 | 132 | return response; 133 | } 134 | 135 | async setNewPassword( 136 | token: string, 137 | setNewPasswordData: SetNewPasswordDto | SetNewPasswordInput, 138 | ): Promise { 139 | const user_email = this.clientService.sendMessageWithPayload( 140 | this.authClient, 141 | { role: 'user', cmd: 'set-new-password' }, 142 | { 143 | forgotPasswordToken: token, 144 | newPassword: setNewPasswordData.new_password, 145 | }, 146 | ); 147 | 148 | const response = this.sendgridService.sendSetNewPasswordEmail({ 149 | customer_email: await user_email, 150 | }); 151 | 152 | return response; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/microservices/auth/strategies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt-refresh-token.strategy'; 2 | export * from './jwt.strategy'; 3 | -------------------------------------------------------------------------------- /src/microservices/auth/strategies/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 | 7 | import { UserService } from '../services/user/user.service'; 8 | import { GetRefreshUserDto } from '../rest/dto'; 9 | import { ITokenPayload } from '../interfaces'; 10 | 11 | @Injectable() 12 | export class JwtRefreshTokenStrategy extends PassportStrategy( 13 | Strategy, 14 | 'jwt-refresh-token', 15 | ) { 16 | constructor( 17 | private readonly configService: ConfigService, 18 | private readonly userService: UserService, 19 | ) { 20 | super({ 21 | jwtFromRequest: ExtractJwt.fromExtractors([ 22 | (request: Request) => { 23 | return request?.cookies?.Refresh; 24 | }, 25 | ]), 26 | secretOrKey: configService.get('JWT_REFRESH_TOKEN_SECRET'), 27 | passReqToCallback: true, 28 | }); 29 | } 30 | 31 | async validate(request: Request, payload: ITokenPayload) { 32 | const refreshTokenObject: GetRefreshUserDto = { 33 | id: payload.userId, 34 | refreshToken: request.cookies?.Refresh, 35 | }; 36 | 37 | return this.userService.getUserIfRefreshTokenMatches(refreshTokenObject); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/microservices/auth/strategies/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 | 7 | import { ITokenPayload } from '../interfaces'; 8 | import { UserService } from '../services/user/user.service'; 9 | 10 | @Injectable() 11 | export class JwtStrategy extends PassportStrategy(Strategy) { 12 | constructor( 13 | private readonly configService: ConfigService, 14 | private readonly userService: UserService, 15 | ) { 16 | super({ 17 | jwtFromRequest: ExtractJwt.fromExtractors([ 18 | (request: Request) => { 19 | return request?.cookies?.Authentication; 20 | }, 21 | ]), 22 | secretOrKey: configService.get('JWT_ACCESS_TOKEN_SECRET'), 23 | }); 24 | } 25 | 26 | async validate(payload: ITokenPayload) { 27 | return this.userService.getUserById(payload.userId); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/microservices/booking/booking.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ClientsModule } from '@nestjs/microservices'; 3 | 4 | import { createClientAsyncOptions } from '@utils/client'; 5 | import { BookingService } from './services'; 6 | import { BookingController } from './rest/controllers'; 7 | import { BookingMutationResolver } from './graphql/mutations'; 8 | import { BookingQueryResolver } from './graphql/queries/'; 9 | 10 | @Module({ 11 | imports: [ 12 | ClientsModule.registerAsync([ 13 | createClientAsyncOptions('auth'), 14 | createClientAsyncOptions('booking'), 15 | ]), 16 | ], 17 | controllers: [BookingController], 18 | providers: [BookingService, BookingQueryResolver, BookingMutationResolver], 19 | exports: [BookingService], 20 | }) 21 | export class BookingModule {} 22 | -------------------------------------------------------------------------------- /src/microservices/booking/graphql/input/create-booking.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | import { IsNumber } from 'class-validator'; 3 | 4 | @InputType() 5 | export class CreateBookingInput { 6 | @Field() 7 | @IsNumber() 8 | customer_id: number; 9 | } 10 | -------------------------------------------------------------------------------- /src/microservices/booking/graphql/input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-booking.input'; 2 | -------------------------------------------------------------------------------- /src/microservices/booking/graphql/models/booking-gql.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql'; 2 | import { IsNumber } from 'class-validator'; 3 | 4 | @ObjectType() 5 | export class GqlBooking { 6 | @Field((type) => ID) 7 | id: number; 8 | 9 | @Field() 10 | @IsNumber() 11 | customer_id: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/microservices/booking/graphql/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './booking-gql.model'; 2 | -------------------------------------------------------------------------------- /src/microservices/booking/graphql/mutations/booking-mutation.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql'; 2 | import { UseGuards } from '@nestjs/common'; 3 | 4 | import { GqlJwtAuthGuard } from '@auth/graphql/guards'; 5 | import { GqlBooking } from '../models'; 6 | import { CreateBookingInput } from '../input/'; 7 | import { BookingService } from '../../services'; 8 | 9 | @Resolver((of) => GqlBooking) 10 | export class BookingMutationResolver { 11 | constructor(private readonly bookingService: BookingService) {} 12 | 13 | @UseGuards(GqlJwtAuthGuard) 14 | @Mutation((returns) => GqlBooking) 15 | async createBooking( 16 | @Args('createBookingData') 17 | createBookingInput: CreateBookingInput, 18 | ): Promise { 19 | const newBooking = await this.bookingService.createBooking( 20 | createBookingInput, 21 | ); 22 | 23 | return newBooking; 24 | } 25 | 26 | @UseGuards(GqlJwtAuthGuard) 27 | @Mutation(() => Boolean) 28 | async deleteBooking(@Args('id') id: number): Promise { 29 | return this.bookingService.deleteBookingById(id); 30 | } 31 | 32 | @UseGuards(GqlJwtAuthGuard) 33 | @Mutation((returns) => GqlBooking) 34 | async updateBooking( 35 | @Args('id') id: number, 36 | @Args('updateBookingData') 37 | updateCustomerProfileInput: CreateBookingInput, 38 | ): Promise { 39 | const updatedCustomerProfile = await this.bookingService.updateBooking( 40 | id, 41 | updateCustomerProfileInput, 42 | ); 43 | 44 | return updatedCustomerProfile; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/microservices/booking/graphql/mutations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './booking-mutation.resolver'; 2 | -------------------------------------------------------------------------------- /src/microservices/booking/graphql/queries/booking-query.resolver.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException, UseGuards } from '@nestjs/common'; 2 | import { Args, Resolver, Int, Query } from '@nestjs/graphql'; 3 | 4 | import { GqlJwtAuthGuard } from '@auth/graphql/guards'; 5 | import { BookingService } from '../../services'; 6 | import { GqlBooking } from '../models'; 7 | 8 | @Resolver((of) => GqlBooking) 9 | export class BookingQueryResolver { 10 | constructor(private readonly bookingService: BookingService) {} 11 | 12 | @UseGuards(GqlJwtAuthGuard) 13 | @Query((returns) => GqlBooking) 14 | async getBooking( 15 | @Args('id', { type: () => Int }) id: number, 16 | ): Promise { 17 | const booking = await this.bookingService.getBookingById(id); 18 | 19 | if (!booking) { 20 | throw new NotFoundException('Customer with that id does not exist'); 21 | } 22 | 23 | return booking; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/microservices/booking/graphql/queries/index.ts: -------------------------------------------------------------------------------- 1 | export * from './booking-query.resolver'; 2 | -------------------------------------------------------------------------------- /src/microservices/booking/rest/controllers/booking.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { createClientAsyncOptions } from '@utils/client'; 5 | import { UtilsModule } from '@utils/utils.module'; 6 | import { BookingService } from '../../services'; 7 | import { BookingController } from './booking.controller'; 8 | 9 | describe('BookingController', () => { 10 | let controller: BookingController; 11 | 12 | beforeEach(async () => { 13 | const module: TestingModule = await Test.createTestingModule({ 14 | imports: [ 15 | ClientsModule.registerAsync([ 16 | createClientAsyncOptions('auth'), 17 | createClientAsyncOptions('booking'), 18 | ]), 19 | UtilsModule, 20 | ], 21 | controllers: [BookingController], 22 | providers: [BookingService], 23 | }).compile(); 24 | 25 | controller = module.get(BookingController); 26 | }); 27 | 28 | it('should be defined', () => { 29 | expect(controller).toBeDefined(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/microservices/booking/rest/controllers/booking.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Param, 4 | Get, 5 | UseGuards, 6 | Delete, 7 | Post, 8 | Put, 9 | Body, 10 | UsePipes, 11 | ValidationPipe, 12 | ParseIntPipe, 13 | } from '@nestjs/common'; 14 | 15 | import { AccessControlGuard } from '@auth/guards'; 16 | import { BookingService } from '../../services'; 17 | import { RestBooking } from '../models'; 18 | import { CreateBookingDto } from '../dto'; 19 | 20 | @Controller('booking') 21 | export class BookingController { 22 | constructor(private readonly bookingService: BookingService) {} 23 | 24 | @UseGuards(AccessControlGuard) 25 | @Get('/:id') 26 | async getBookingById( 27 | @Param('id', ParseIntPipe) id: number, 28 | ): Promise { 29 | return this.bookingService.getBookingById(id); 30 | } 31 | 32 | @UsePipes(new ValidationPipe()) 33 | @UseGuards(AccessControlGuard) 34 | @Post('/') 35 | async createBooking( 36 | @Body() newBooking: CreateBookingDto, 37 | ): Promise { 38 | return this.bookingService.createBooking(newBooking); 39 | } 40 | 41 | @UsePipes(new ValidationPipe()) 42 | @UseGuards(AccessControlGuard) 43 | @Put('/:id') 44 | async updateBooking( 45 | @Param('id', ParseIntPipe) id: number, 46 | @Body() updatedBooking: CreateBookingDto, 47 | ): Promise { 48 | return this.bookingService.updateBooking(id, updatedBooking); 49 | } 50 | 51 | @UsePipes(new ValidationPipe()) 52 | @UseGuards(AccessControlGuard) 53 | @Delete('/:id') 54 | async deleteBookingById( 55 | @Param('id', ParseIntPipe) id: number, 56 | ): Promise { 57 | return this.bookingService.deleteBookingById(id); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/microservices/booking/rest/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './booking.controller'; 2 | -------------------------------------------------------------------------------- /src/microservices/booking/rest/dto/create-booking.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsPositive } from 'class-validator'; 2 | 3 | export class CreateBookingDto { 4 | @IsPositive() 5 | customer_id: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/microservices/booking/rest/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-booking.dto'; 2 | -------------------------------------------------------------------------------- /src/microservices/booking/rest/models/booking-rest.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsInt, IsNumber } from 'class-validator'; 2 | 3 | export class RestBooking { 4 | @IsInt() 5 | id: number; 6 | 7 | @IsDate() 8 | date: Date; 9 | 10 | @IsNumber() 11 | customer_id: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/microservices/booking/rest/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './booking-rest'; 2 | -------------------------------------------------------------------------------- /src/microservices/booking/services/booking.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { createClientAsyncOptions } from '@utils/client'; 5 | import { BookingService } from './booking.service'; 6 | 7 | describe('BookingService', () => { 8 | let service: BookingService; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | imports: [ 13 | ClientsModule.registerAsync([ 14 | createClientAsyncOptions('auth'), 15 | createClientAsyncOptions('booking'), 16 | ]), 17 | ], 18 | providers: [BookingService], 19 | }).compile(); 20 | 21 | service = module.get(BookingService); 22 | }); 23 | 24 | it('should be defined', () => { 25 | expect(service).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/microservices/booking/services/booking.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, HttpException } from '@nestjs/common'; 2 | import { ClientProxy } from '@nestjs/microservices'; 3 | 4 | import { CreateBookingDto } from '../rest/dto'; 5 | import { RestBooking } from '../rest/models'; 6 | 7 | @Injectable() 8 | export class BookingService { 9 | constructor( 10 | @Inject('BOOKING_MICROSERVICE') 11 | public readonly bookingClient: ClientProxy, 12 | ) {} 13 | 14 | async getBookingById(id: number): Promise { 15 | try { 16 | const booking = await this.bookingClient 17 | .send({ role: 'booking', cmd: 'get' }, id) 18 | .toPromise(); 19 | 20 | return booking; 21 | } catch (error) { 22 | throw new HttpException(error.errorStatus, error.statusCode); 23 | } 24 | } 25 | 26 | async createBooking(newBooking: CreateBookingDto): Promise { 27 | try { 28 | const booking = await this.bookingClient 29 | .send({ role: 'booking', cmd: 'create' }, newBooking) 30 | .toPromise(); 31 | 32 | return booking; 33 | } catch (error) { 34 | throw new HttpException(error.errorStatus, error.statusCode); 35 | } 36 | } 37 | 38 | async updateBooking( 39 | id: number, 40 | updatedBooking: CreateBookingDto, 41 | ): Promise { 42 | try { 43 | const booking = await this.bookingClient 44 | .send({ role: 'booking', cmd: 'update' }, { id, updatedBooking }) 45 | .toPromise(); 46 | 47 | return booking; 48 | } catch (error) { 49 | throw new HttpException(error.errorStatus, error.statusCode); 50 | } 51 | } 52 | 53 | async deleteBookingById(id: number): Promise { 54 | try { 55 | const booking = await this.bookingClient 56 | .send({ role: 'booking', cmd: 'remove' }, id) 57 | .toPromise(); 58 | 59 | return booking; 60 | } catch (error) { 61 | throw new HttpException(error.errorStatus, error.statusCode); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/microservices/booking/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './booking.service'; 2 | -------------------------------------------------------------------------------- /src/microservices/catalog/catalog.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ClientsModule } from '@nestjs/microservices'; 3 | 4 | import { OfferController } from './rest/controllers'; 5 | import { OfferService } from './services'; 6 | import { OfferMutationResolver } from './graphql/mutations'; 7 | import { OfferQueryResolver } from './graphql/queries'; 8 | import { createClientAsyncOptions } from '@utils/client'; 9 | 10 | @Module({ 11 | imports: [ClientsModule.registerAsync([createClientAsyncOptions('catalog')])], 12 | controllers: [OfferController], 13 | providers: [OfferService, OfferMutationResolver, OfferQueryResolver], 14 | }) 15 | export class CatalogModule {} 16 | -------------------------------------------------------------------------------- /src/microservices/catalog/graphql/input/create-offer.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | import { IsString, Length } from 'class-validator'; 3 | 4 | @InputType() 5 | export class CreateOfferInput { 6 | @Field() 7 | @IsString() 8 | @Length(5, 30) 9 | name: string; 10 | 11 | @Field() 12 | @IsString() 13 | @Length(20, 200) 14 | description: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/microservices/catalog/graphql/input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-offer.input'; 2 | export * from './update-offer.input'; 3 | -------------------------------------------------------------------------------- /src/microservices/catalog/graphql/input/update-offer.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | import { IsString, Length } from 'class-validator'; 3 | 4 | @InputType() 5 | export class UpdateOfferInput { 6 | @Field() 7 | @IsString() 8 | @Length(5, 30) 9 | name: string; 10 | 11 | @Field() 12 | @IsString() 13 | @Length(20, 200) 14 | description: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/microservices/catalog/graphql/models/gql-offer.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class GqlOfferModel { 5 | @Field((type) => ID) 6 | activity_id: number; 7 | 8 | @Field() 9 | name: string; 10 | 11 | @Field() 12 | description: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/microservices/catalog/graphql/models/gql-text-response.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class GqlTextResponseModel { 5 | @Field() 6 | response: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/microservices/catalog/graphql/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gql-offer.model'; 2 | export * from './gql-text-response.model'; 3 | -------------------------------------------------------------------------------- /src/microservices/catalog/graphql/mutations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './offer-mutation.resolver'; 2 | -------------------------------------------------------------------------------- /src/microservices/catalog/graphql/mutations/offer-mutation.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql'; 2 | 3 | import { OfferService } from '../../services'; 4 | import { CreateOfferInput, UpdateOfferInput } from '../input'; 5 | import { GqlOfferModel, GqlTextResponseModel } from '../models'; 6 | 7 | @Resolver() 8 | export class OfferMutationResolver { 9 | constructor(private readonly offerService: OfferService) {} 10 | 11 | @Mutation((returns) => GqlOfferModel) 12 | async createOffer( 13 | @Args('createOfferInput') createOfferInput: CreateOfferInput, 14 | ): Promise { 15 | return this.offerService.createOffer(createOfferInput); 16 | } 17 | 18 | @Mutation((returns) => GqlOfferModel) 19 | async updateOffer( 20 | @Args('id') id: number, 21 | @Args('updateOfferInput') updateOfferInput: UpdateOfferInput, 22 | ): Promise { 23 | return this.offerService.updateOffer(id, updateOfferInput); 24 | } 25 | 26 | @Mutation((returns) => GqlTextResponseModel) 27 | async deleteOffer(@Args('id') id: number): Promise { 28 | return this.offerService.deleteOffer(id); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/microservices/catalog/graphql/queries/index.ts: -------------------------------------------------------------------------------- 1 | export * from './offer-query.resolver'; 2 | -------------------------------------------------------------------------------- /src/microservices/catalog/graphql/queries/offer-query.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Query, Resolver } from '@nestjs/graphql'; 2 | 3 | import { OfferService } from '../../services'; 4 | import { GqlOfferModel } from '../models'; 5 | 6 | @Resolver() 7 | export class OfferQueryResolver { 8 | constructor(private readonly offerService: OfferService) {} 9 | 10 | @Query((returns) => GqlOfferModel) 11 | async getSingleOffer(@Args('id') id: number): Promise { 12 | return this.offerService.getSingleOffer(id); 13 | } 14 | 15 | @Query((returns) => GqlOfferModel) 16 | async getAllOffers(): Promise { 17 | return this.offerService.getAllOffers(); 18 | } 19 | 20 | @Query((returns) => GqlOfferModel) 21 | async getOffersByQuery( 22 | @Args('query') query: string, 23 | ): Promise { 24 | return this.offerService.getOffersByQuery(query); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/microservices/catalog/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './update-offer.interface'; 2 | -------------------------------------------------------------------------------- /src/microservices/catalog/interfaces/update-offer.interface.ts: -------------------------------------------------------------------------------- 1 | import { UpdateOfferDto } from '../rest/dto'; 2 | /** 3 | * @interface IUpdateActivity 4 | * 5 | * @property {number} id 6 | * @property {UpdateOfferDto} updateOfferDto 7 | */ 8 | export interface IUpdateOffer { 9 | id: number; 10 | updateOfferDto: UpdateOfferDto; 11 | } 12 | -------------------------------------------------------------------------------- /src/microservices/catalog/rest/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './offer.controller'; 2 | -------------------------------------------------------------------------------- /src/microservices/catalog/rest/controllers/offer.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { OfferService } from '../../services'; 5 | import { OfferController } from './offer.controller'; 6 | import { RedisCacheModule } from '../../../../cache/redis-cache.module'; 7 | import { createClientAsyncOptions } from '../../../../utils/client'; 8 | import { UtilsModule } from '../../../../utils/utils.module'; 9 | 10 | describe('OfferController', () => { 11 | let controller: OfferController; 12 | 13 | beforeEach(async () => { 14 | const module: TestingModule = await Test.createTestingModule({ 15 | imports: [ 16 | ClientsModule.registerAsync([createClientAsyncOptions('catalog')]), 17 | RedisCacheModule, 18 | UtilsModule, 19 | ], 20 | controllers: [OfferController], 21 | providers: [OfferService], 22 | }).compile(); 23 | 24 | controller = module.get(OfferController); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(controller).toBeDefined(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/microservices/catalog/rest/controllers/offer.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Post, 8 | Put, 9 | Query, 10 | } from '@nestjs/common'; 11 | 12 | import { OfferService } from '../../services'; 13 | import { CreateOfferDto, UpdateOfferDto } from '../dto'; 14 | import { RestOfferModel } from '../models'; 15 | import { RestTextResponseModel } from '../models'; 16 | 17 | @Controller('offer') 18 | export class OfferController { 19 | constructor(private readonly offerService: OfferService) {} 20 | 21 | @Get('/all-offers') 22 | async getAllOffers(): Promise { 23 | return this.offerService.getAllOffers(); 24 | } 25 | 26 | @Get('/offers-by-query') 27 | async getOffersByQuery( 28 | @Query('query') query: string, 29 | ): Promise { 30 | return this.offerService.getOffersByQuery(query); 31 | } 32 | 33 | @Get('/:id') 34 | async getSingleOffer(@Param('id') id: number): Promise { 35 | return this.offerService.getSingleOffer(id); 36 | } 37 | 38 | @Post('/create') 39 | async createOffer( 40 | @Body() createOfferDto: CreateOfferDto, 41 | ): Promise { 42 | return this.offerService.createOffer(createOfferDto); 43 | } 44 | 45 | @Put('/:id/update') 46 | async updateOffer( 47 | @Param('id') id: number, 48 | @Body() updateOfferDto: UpdateOfferDto, 49 | ): Promise { 50 | return this.offerService.updateOffer(id, updateOfferDto); 51 | } 52 | 53 | @Delete('/:id/delete') 54 | async deleteOffer(@Param('id') id: number): Promise { 55 | return this.offerService.deleteOffer(id); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/microservices/catalog/rest/dto/create-offer.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, Length } from 'class-validator'; 2 | 3 | export class CreateOfferDto { 4 | @IsString() 5 | @Length(5, 30) 6 | name: string; 7 | 8 | @IsString() 9 | @Length(20, 200) 10 | description: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/microservices/catalog/rest/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-offer.dto'; 2 | export * from './update-offer.dto'; 3 | -------------------------------------------------------------------------------- /src/microservices/catalog/rest/dto/update-offer.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, Length } from 'class-validator'; 2 | 3 | export class UpdateOfferDto { 4 | @IsString() 5 | @Length(5, 30) 6 | name: string; 7 | 8 | @IsString() 9 | @Length(20, 200) 10 | description: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/microservices/catalog/rest/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rest-offer.model'; 2 | export * from './rest-text-response.model'; 3 | -------------------------------------------------------------------------------- /src/microservices/catalog/rest/models/rest-offer.model.ts: -------------------------------------------------------------------------------- 1 | import { IsInt, IsString } from 'class-validator'; 2 | 3 | export class RestOfferModel { 4 | @IsInt() 5 | activity_id: number; 6 | 7 | @IsString() 8 | name: string; 9 | 10 | @IsString() 11 | description: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/microservices/catalog/rest/models/rest-text-response.model.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | export class RestTextResponseModel { 4 | @IsString() 5 | response: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/microservices/catalog/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './offer.service'; 2 | -------------------------------------------------------------------------------- /src/microservices/catalog/services/offer.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { RedisCacheModule } from '../../../cache/redis-cache.module'; 5 | import { OfferService } from './offer.service'; 6 | import { createClientAsyncOptions } from '../../../utils/client'; 7 | import { UtilsModule } from '../../../utils/utils.module'; 8 | 9 | describe('OfferService', () => { 10 | let service: OfferService; 11 | 12 | beforeEach(async () => { 13 | const module: TestingModule = await Test.createTestingModule({ 14 | imports: [ 15 | ClientsModule.registerAsync([createClientAsyncOptions('catalog')]), 16 | RedisCacheModule, 17 | UtilsModule, 18 | ], 19 | providers: [OfferService], 20 | }).compile(); 21 | 22 | service = module.get(OfferService); 23 | }); 24 | 25 | it('should be defined', () => { 26 | expect(service).toBeDefined(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/microservices/catalog/services/offer.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CacheInterceptor, 3 | CacheKey, 4 | CacheTTL, 5 | Inject, 6 | Injectable, 7 | UseInterceptors, 8 | } from '@nestjs/common'; 9 | import { ClientProxy } from '@nestjs/microservices'; 10 | 11 | import { CreateOfferInput, UpdateOfferInput } from '../graphql/input'; 12 | import { GqlOfferModel, GqlTextResponseModel } from '../graphql/models'; 13 | import { IUpdateOffer } from '../interfaces'; 14 | import { CreateOfferDto, UpdateOfferDto } from '../rest/dto'; 15 | import { RestOfferModel } from '../rest/models'; 16 | import { RestTextResponseModel } from '../rest/models'; 17 | import { ClientService } from '@utils/client'; 18 | 19 | @Injectable() 20 | export class OfferService { 21 | constructor( 22 | @Inject('CATALOG_MICROSERVICE') 23 | private readonly catalogClient: ClientProxy, 24 | private readonly clientService: ClientService, 25 | ) {} 26 | 27 | async getSingleOffer(id: number): Promise { 28 | return this.clientService.sendMessageWithPayload( 29 | this.catalogClient, 30 | { role: 'offer', cmd: 'getSingle' }, 31 | id, 32 | ); 33 | } 34 | 35 | @UseInterceptors(CacheInterceptor) 36 | @CacheKey('all-offers') 37 | @CacheTTL(20) 38 | async getAllOffers(): Promise { 39 | return this.clientService.sendMessageWithPayload( 40 | this.catalogClient, 41 | { role: 'offer', cmd: 'getAll' }, 42 | {}, 43 | ); 44 | } 45 | 46 | async getOffersByQuery( 47 | query: string, 48 | ): Promise { 49 | return this.clientService.sendMessageWithPayload( 50 | this.catalogClient, 51 | { role: 'offer', cmd: 'getOfferByQuery' }, 52 | query, 53 | ); 54 | } 55 | 56 | async createOffer( 57 | createOfferData: CreateOfferDto | CreateOfferInput, 58 | ): Promise { 59 | return this.clientService.sendMessageWithPayload( 60 | this.catalogClient, 61 | { role: 'activity', cmd: 'create' }, 62 | createOfferData, 63 | ); 64 | } 65 | 66 | async updateOffer( 67 | id: number, 68 | updateOfferData: UpdateOfferDto | UpdateOfferInput, 69 | ): Promise { 70 | const updateOfferObject: IUpdateOffer = { 71 | id, 72 | updateOfferDto: updateOfferData, 73 | }; 74 | return this.clientService.sendMessageWithPayload( 75 | this.catalogClient, 76 | { role: 'offer', cmd: 'update' }, 77 | updateOfferObject, 78 | ); 79 | } 80 | 81 | async deleteOffer( 82 | id: number, 83 | ): Promise { 84 | return this.clientService.sendMessageWithPayload( 85 | this.catalogClient, 86 | { role: 'offer', cmd: 'delete' }, 87 | id, 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/microservices/customer/customer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ClientsModule } from '@nestjs/microservices'; 3 | 4 | import { createClientAsyncOptions } from '@utils/client'; 5 | import { CustomerController } from './rest/controllers'; 6 | import { CustomerService } from './services'; 7 | import { CustomerQueryResolver } from './graphql/queries'; 8 | import { CustomerMutationResolver } from './graphql/mutations'; 9 | 10 | @Module({ 11 | imports: [ 12 | ClientsModule.registerAsync([ 13 | createClientAsyncOptions('auth'), 14 | createClientAsyncOptions('customer'), 15 | ]), 16 | ], 17 | controllers: [CustomerController], 18 | providers: [CustomerService, CustomerQueryResolver, CustomerMutationResolver], 19 | }) 20 | export class CustomerModule {} 21 | -------------------------------------------------------------------------------- /src/microservices/customer/graphql/input/create-customer-profile.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | import { MaxLength } from 'class-validator'; 3 | 4 | @InputType() 5 | export class CreateCustomerProfileInput { 6 | @Field() 7 | @MaxLength(30) 8 | first_name: string; 9 | 10 | @Field() 11 | @MaxLength(30) 12 | last_name: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/microservices/customer/graphql/input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-customer-profile.input'; 2 | export * from './update-customer-profile.input'; 3 | -------------------------------------------------------------------------------- /src/microservices/customer/graphql/input/update-customer-profile.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | import { MaxLength } from 'class-validator'; 3 | 4 | @InputType() 5 | export class UpdateCustomerProfileInput { 6 | @Field() 7 | @MaxLength(30) 8 | first_name: string; 9 | 10 | @Field() 11 | @MaxLength(30) 12 | last_name: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/microservices/customer/graphql/models/customer-gql.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class GqlCustomer { 5 | @Field((type) => ID) 6 | id: number; 7 | 8 | @Field() 9 | first_name: string; 10 | 11 | @Field() 12 | last_name: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/microservices/customer/graphql/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './customer-gql.model'; 2 | -------------------------------------------------------------------------------- /src/microservices/customer/graphql/mutations/customer-mutation.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql'; 2 | import { UseGuards } from '@nestjs/common'; 3 | 4 | import { GqlJwtAuthGuard } from '@auth/graphql/guards'; 5 | import { GqlCustomer } from '../models'; 6 | import { CustomerService } from '../../services'; 7 | import { 8 | CreateCustomerProfileInput, 9 | UpdateCustomerProfileInput, 10 | } from '../input'; 11 | 12 | @Resolver((of) => GqlCustomer) 13 | export class CustomerMutationResolver { 14 | constructor(private readonly customerService: CustomerService) {} 15 | 16 | @Mutation((returns) => GqlCustomer) 17 | async createCustomerProfile( 18 | @Args('createCustomerProfileData') 19 | createCustomerProfileInput: CreateCustomerProfileInput, 20 | ): Promise { 21 | const newCustomerProfile = await this.customerService.createCustomerProfile( 22 | createCustomerProfileInput, 23 | ); 24 | 25 | return newCustomerProfile; 26 | } 27 | 28 | @UseGuards(GqlJwtAuthGuard) 29 | @Mutation((returns) => Boolean) 30 | async removeCustomerProfile(@Args('id') id: number): Promise { 31 | return this.customerService.removeCustomerProfile(id); 32 | } 33 | 34 | @UseGuards(GqlJwtAuthGuard) 35 | @Mutation((returns) => GqlCustomer) 36 | async updateCustomerProfile( 37 | @Args('id') id: number, 38 | @Args('updateCustomerProfileData') 39 | updateCustomerProfileInput: UpdateCustomerProfileInput, 40 | ): Promise { 41 | const updatedCustomerProfile = await this.customerService.updateCustomerProfile( 42 | id, 43 | updateCustomerProfileInput, 44 | ); 45 | 46 | return updatedCustomerProfile; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/microservices/customer/graphql/mutations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './customer-mutation.resolver'; 2 | -------------------------------------------------------------------------------- /src/microservices/customer/graphql/queries/customer-query.resolver.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException, UseGuards } from '@nestjs/common'; 2 | import { Args, Resolver, Int, Query } from '@nestjs/graphql'; 3 | 4 | import { GqlJwtAuthGuard } from '@auth/graphql/guards'; 5 | import { GqlCustomer } from '../models'; 6 | import { CustomerService } from '../../services'; 7 | 8 | @Resolver((of) => GqlCustomer) 9 | export class CustomerQueryResolver { 10 | constructor(private readonly customerService: CustomerService) {} 11 | 12 | @UseGuards(GqlJwtAuthGuard) 13 | @Query((returns) => GqlCustomer) 14 | async getCustomerProfile( 15 | @Args('id', { type: () => Int }) id: number, 16 | ): Promise { 17 | const customer = await this.customerService.getCustomerProfile(id); 18 | 19 | if (!customer) { 20 | throw new NotFoundException('Customer with that id does not exist'); 21 | } 22 | 23 | return customer; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/microservices/customer/graphql/queries/index.ts: -------------------------------------------------------------------------------- 1 | export * from './customer-query.resolver'; 2 | -------------------------------------------------------------------------------- /src/microservices/customer/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './update-customer-object.interface'; 2 | -------------------------------------------------------------------------------- /src/microservices/customer/interfaces/update-customer-object.interface.ts: -------------------------------------------------------------------------------- 1 | import { UpdateCustomerProfileInput } from '../graphql/input/update-customer-profile.input'; 2 | import { UpdateCustomerProfileDto } from '../rest/dto/update-customer-profile.dto'; 3 | 4 | export interface IUpdateCustomerObject { 5 | id: number; 6 | updateCustomerProfileData: 7 | | UpdateCustomerProfileDto 8 | | UpdateCustomerProfileInput; 9 | } 10 | -------------------------------------------------------------------------------- /src/microservices/customer/rest/controllers/customer.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { createClientAsyncOptions } from '@utils/client'; 5 | import { CustomerService } from '../../services'; 6 | import { CustomerController } from './customer.controller'; 7 | 8 | describe('CustomerController', () => { 9 | let controller: CustomerController; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | imports: [ 14 | ClientsModule.registerAsync([ 15 | createClientAsyncOptions('auth'), 16 | createClientAsyncOptions('customer'), 17 | ]), 18 | ], 19 | controllers: [CustomerController], 20 | providers: [CustomerService], 21 | }).compile(); 22 | 23 | controller = module.get(CustomerController); 24 | }); 25 | 26 | it('should be defined', () => { 27 | expect(controller).toBeDefined(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/microservices/customer/rest/controllers/customer.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Param, 4 | Get, 5 | UseGuards, 6 | Post, 7 | Body, 8 | Delete, 9 | Put, 10 | } from '@nestjs/common'; 11 | 12 | import { RestJwtAuthGuard } from '@auth/rest/guards'; 13 | import { CustomerService } from '../../services'; 14 | import { CreateCustomerProfileDto, UpdateCustomerProfileDto } from '../dto'; 15 | import { RestCustomer } from '../models'; 16 | 17 | @Controller('customer') 18 | export class CustomerController { 19 | constructor(private readonly customerService: CustomerService) {} 20 | 21 | @UseGuards(RestJwtAuthGuard) 22 | @Get('/profile/:id') 23 | async getCustomerProfile(@Param('id') id: number): Promise { 24 | return this.customerService.getCustomerProfile(id); 25 | } 26 | 27 | @Post('/create') 28 | async createCustomerProfile( 29 | @Body() createCustomerProfileDto: CreateCustomerProfileDto, 30 | ): Promise { 31 | return this.customerService.createCustomerProfile(createCustomerProfileDto); 32 | } 33 | 34 | @UseGuards(RestJwtAuthGuard) 35 | @Delete('/delete/:id') 36 | async removeCustomerProfile(@Param('id') id: number): Promise { 37 | return this.customerService.removeCustomerProfile(id); 38 | } 39 | 40 | @UseGuards(RestJwtAuthGuard) 41 | @Put('/update/:id') 42 | async updateCustomerProfile( 43 | @Param('id') id: number, 44 | @Body() updateCustomerProfileDto: UpdateCustomerProfileDto, 45 | ): Promise { 46 | return this.customerService.updateCustomerProfile( 47 | id, 48 | updateCustomerProfileDto, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/microservices/customer/rest/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './customer.controller'; 2 | -------------------------------------------------------------------------------- /src/microservices/customer/rest/dto/create-customer-profile.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateCustomerProfileDto { 2 | auth_id: number; 3 | first_name: string; 4 | last_name: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/microservices/customer/rest/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-customer-profile.dto'; 2 | export * from './update-customer-profile.dto'; 3 | -------------------------------------------------------------------------------- /src/microservices/customer/rest/dto/update-customer-profile.dto.ts: -------------------------------------------------------------------------------- 1 | export class UpdateCustomerProfileDto { 2 | first_name: string; 3 | last_name: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/microservices/customer/rest/models/customer-rest.model.ts: -------------------------------------------------------------------------------- 1 | import { IsInt, Length } from 'class-validator'; 2 | 3 | export class RestCustomer { 4 | @IsInt() 5 | id: number; 6 | 7 | @Length(5, 30) 8 | first_name: string; 9 | 10 | @Length(5, 30) 11 | last_name: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/microservices/customer/rest/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './customer-rest.model'; 2 | -------------------------------------------------------------------------------- /src/microservices/customer/services/customer.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { createClientAsyncOptions } from '@utils/client'; 5 | import { CustomerService } from './customer.service'; 6 | 7 | describe('CustomerService', () => { 8 | let service: CustomerService; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | imports: [ 13 | ClientsModule.registerAsync([ 14 | createClientAsyncOptions('auth'), 15 | createClientAsyncOptions('customer'), 16 | ]), 17 | ], 18 | providers: [CustomerService], 19 | }).compile(); 20 | 21 | service = module.get(CustomerService); 22 | }); 23 | 24 | it('should be defined', () => { 25 | expect(service).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/microservices/customer/services/customer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, BadRequestException } from '@nestjs/common'; 2 | import { ClientProxy } from '@nestjs/microservices'; 3 | 4 | import { 5 | CreateCustomerProfileDto, 6 | UpdateCustomerProfileDto, 7 | } from '../rest/dto'; 8 | import { 9 | CreateCustomerProfileInput, 10 | UpdateCustomerProfileInput, 11 | } from '../graphql/input'; 12 | import { RestCustomer } from '../rest/models'; 13 | import { GqlCustomer } from '../graphql/models'; 14 | import { IUpdateCustomerObject } from '../interfaces'; 15 | 16 | @Injectable() 17 | export class CustomerService { 18 | constructor( 19 | @Inject('CUSTOMER_MICROSERVICE') 20 | public readonly customerClient: ClientProxy, 21 | ) {} 22 | 23 | async getCustomerProfile(id: number): Promise { 24 | const customerProfile = await this.customerClient 25 | .send({ role: 'customer', cmd: 'get' }, id) 26 | .toPromise(); 27 | return customerProfile; 28 | } 29 | 30 | async createCustomerProfile( 31 | createCustomerProfileData: 32 | | CreateCustomerProfileInput 33 | | CreateCustomerProfileDto, 34 | ): Promise { 35 | const newCustomerProfile = await this.customerClient 36 | .send({ role: 'customer', cmd: 'create' }, createCustomerProfileData) 37 | .toPromise(); 38 | if (!newCustomerProfile) throw new BadRequestException(); // Change to more appropriate exception 39 | return newCustomerProfile; 40 | } 41 | 42 | async removeCustomerProfile(id: number): Promise { 43 | const customerRemoved = await this.customerClient 44 | .send({ role: 'customer', cmd: 'remove' }, id) 45 | .toPromise(); 46 | return customerRemoved; 47 | } 48 | 49 | async updateCustomerProfile( 50 | id: number, 51 | updateCustomerProfileData: 52 | | UpdateCustomerProfileDto 53 | | UpdateCustomerProfileInput, 54 | ): Promise { 55 | const updateProfileObject: IUpdateCustomerObject = { 56 | id, 57 | updateCustomerProfileData, 58 | }; 59 | const updatedProfile = await this.customerClient 60 | .send({ role: 'customer', cmd: 'update' }, updateProfileObject) 61 | .toPromise(); 62 | return updatedProfile; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/microservices/customer/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './customer.service'; 2 | -------------------------------------------------------------------------------- /src/microservices/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthModule } from './auth/auth.module'; 2 | import { BookingModule } from './booking/booking.module'; 3 | import { CatalogModule } from './catalog/catalog.module'; 4 | import { CustomerModule } from './customer/customer.module'; 5 | import { MailModule } from './mail/mail.module'; 6 | import { PaymentModule } from './payment/payment.module'; 7 | 8 | export const MicroservicesModules = [ 9 | AuthModule, 10 | BookingModule, 11 | CatalogModule, 12 | CustomerModule, 13 | MailModule, 14 | PaymentModule, 15 | ]; 16 | -------------------------------------------------------------------------------- /src/microservices/mail/mail.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { UtilsModule } from '@utils/utils.module'; 4 | import { SendgridModule } from './sendgrid/sendgrid.module'; 5 | 6 | @Module({ 7 | imports: [SendgridModule, UtilsModule], 8 | exports: [SendgridModule], 9 | }) 10 | export class MailModule {} 11 | -------------------------------------------------------------------------------- /src/microservices/mail/sendgrid/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './send-email.dto'; 2 | -------------------------------------------------------------------------------- /src/microservices/mail/sendgrid/dto/send-email.dto.ts: -------------------------------------------------------------------------------- 1 | export class SendEmailDto { 2 | customer_email: string; 3 | token?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/microservices/mail/sendgrid/interfaces/mail-object.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IMailObject { 2 | customer_email: string; 3 | email_type: string; 4 | confirmation_token: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/microservices/mail/sendgrid/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './success-response.model'; 2 | -------------------------------------------------------------------------------- /src/microservices/mail/sendgrid/models/success-response.model.ts: -------------------------------------------------------------------------------- 1 | export class SuccessResponseModel { 2 | response: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/microservices/mail/sendgrid/sendgrid.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ClientsModule } from '@nestjs/microservices'; 3 | 4 | import { createClientAsyncOptions } from '@utils/client'; 5 | import { SendgridService } from './services/sendgrid.service'; 6 | 7 | @Module({ 8 | imports: [ClientsModule.registerAsync([createClientAsyncOptions('mail')])], 9 | providers: [SendgridService], 10 | exports: [SendgridService], 11 | }) 12 | export class SendgridModule {} 13 | -------------------------------------------------------------------------------- /src/microservices/mail/sendgrid/services/sendgrid.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { createClientAsyncOptions } from '../../../../utils/client'; 5 | import { UtilsModule } from '../../../../utils/utils.module'; 6 | import { SendgridService } from './sendgrid.service'; 7 | 8 | describe('SendgridService', () => { 9 | let service: SendgridService; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | imports: [ 14 | ClientsModule.registerAsync([createClientAsyncOptions('mail')]), 15 | UtilsModule, 16 | ], 17 | providers: [SendgridService], 18 | }).compile(); 19 | 20 | service = module.get(SendgridService); 21 | }); 22 | 23 | it('should be defined', () => { 24 | expect(service).toBeDefined(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/microservices/mail/sendgrid/services/sendgrid.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { ClientProxy } from '@nestjs/microservices'; 3 | 4 | import { ClientService } from '@utils/client'; 5 | import { SendEmailDto } from '../dto'; 6 | import { SuccessResponseModel } from '../models'; 7 | 8 | @Injectable() 9 | export class SendgridService { 10 | constructor( 11 | @Inject('MAIL_MICROSERVICE') 12 | private readonly mailClient: ClientProxy, 13 | private readonly clientService: ClientService, 14 | ) {} 15 | 16 | async sendConfirmCreateAccountEmail( 17 | sendEmailDto: SendEmailDto, 18 | ): Promise { 19 | return this.clientService.sendMessageWithPayload( 20 | this.mailClient, 21 | { role: 'mail', cmd: 'send', type: 'confirmation' }, 22 | sendEmailDto, 23 | ); 24 | } 25 | 26 | async sendForgotPasswordEmail( 27 | sendEmailDto: SendEmailDto, 28 | ): Promise { 29 | return this.clientService.sendMessageWithPayload( 30 | this.mailClient, 31 | { role: 'mail', cmd: 'send', type: 'forgot-password' }, 32 | sendEmailDto, 33 | ); 34 | } 35 | 36 | async sendSetNewPasswordEmail( 37 | sendEmailDto: SendEmailDto, 38 | ): Promise { 39 | return this.clientService.sendMessageWithPayload( 40 | this.mailClient, 41 | { role: 'mail', cmd: 'send', type: 'set-new-password' }, 42 | sendEmailDto, 43 | ); 44 | } 45 | 46 | async sendConfirmBookingEmail( 47 | sendEmailDto: SendEmailDto, 48 | ): Promise { 49 | return this.clientService.sendMessageWithPayload( 50 | this.mailClient, 51 | { role: 'mail', cmd: 'send', type: 'confirm-booking' }, 52 | sendEmailDto, 53 | ); 54 | } 55 | 56 | async sendDeleteAccountMail( 57 | sendEmailDto: SendEmailDto, 58 | ): Promise { 59 | return this.clientService.sendMessageWithPayload( 60 | this.mailClient, 61 | { role: 'mail', cmd: 'send', type: 'delete-account' }, 62 | sendEmailDto, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/microservices/payment/graphql/input/create-payment.input.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from '@nestjs/graphql'; 2 | import { IsNumber, IsString } from 'class-validator'; 3 | 4 | @InputType() 5 | export class CreatePaymentInput { 6 | @Field() 7 | @IsNumber() 8 | booking_id: number; 9 | 10 | @Field() 11 | @IsNumber() 12 | amount: number; 13 | 14 | @Field() 15 | @IsString() 16 | card_token: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/microservices/payment/graphql/input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-payment.input'; 2 | -------------------------------------------------------------------------------- /src/microservices/payment/graphql/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './payment-gql.model'; 2 | -------------------------------------------------------------------------------- /src/microservices/payment/graphql/models/payment-gql.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql'; 2 | import { IsDate, IsNumber } from 'class-validator'; 3 | 4 | @ObjectType() 5 | export class GqlPayment { 6 | @Field((type) => ID) 7 | id: number; 8 | 9 | @Field() 10 | @IsNumber() 11 | booking_id: number; 12 | 13 | @Field() 14 | @IsDate() 15 | date: Date; 16 | } 17 | -------------------------------------------------------------------------------- /src/microservices/payment/graphql/mutations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './payment-mutation.resolver'; 2 | -------------------------------------------------------------------------------- /src/microservices/payment/graphql/mutations/payment-mutation.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql'; 2 | import { UseGuards } from '@nestjs/common'; 3 | 4 | import { GqlJwtAuthGuard } from '@auth/graphql/guards'; 5 | import { GqlPayment } from '../models'; 6 | import { CreatePaymentInput } from '../input'; 7 | import { PaymentService } from '../../services'; 8 | 9 | @Resolver((of) => GqlPayment) 10 | export class PaymentMutationResolver { 11 | constructor(private readonly paymentService: PaymentService) {} 12 | 13 | @UseGuards(GqlJwtAuthGuard) 14 | @Mutation((returns) => GqlPayment) 15 | async createPayment( 16 | @Args('createPaymentData') createPaymentInput: CreatePaymentInput, 17 | ): Promise { 18 | return await this.paymentService.createPayment(createPaymentInput); 19 | } 20 | 21 | @UseGuards(GqlJwtAuthGuard) 22 | @Mutation((returns) => GqlPayment) 23 | async updatePayment( 24 | @Args('id') id: number, 25 | @Args('updatePaymentData') updatePaymentInput: CreatePaymentInput, 26 | ): Promise { 27 | return this.paymentService.updatePayment(id, updatePaymentInput); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/microservices/payment/graphql/queries/index.ts: -------------------------------------------------------------------------------- 1 | export * from './payment-query.resolver'; 2 | -------------------------------------------------------------------------------- /src/microservices/payment/graphql/queries/payment-query.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common'; 2 | import { Args, Resolver, Int, Query } from '@nestjs/graphql'; 3 | 4 | import { GqlJwtAuthGuard } from '@auth/graphql/guards'; 5 | import { PaymentService } from '../../services'; 6 | import { GqlPayment } from '../models'; 7 | 8 | @Resolver((of) => GqlPayment) 9 | export class PaymentQueryResolver { 10 | constructor(private readonly paymentService: PaymentService) {} 11 | 12 | @UseGuards(GqlJwtAuthGuard) 13 | @Query((returns) => GqlPayment) 14 | async getPayment( 15 | @Args('id', { type: () => Int }) id: number, 16 | ): Promise { 17 | return this.paymentService.getPaymentById(id); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/microservices/payment/payment.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ClientsModule } from '@nestjs/microservices'; 3 | 4 | import { createClientAsyncOptions } from '@utils/client'; 5 | import { BookingModule } from '@booking/booking.module'; 6 | import { PaymentService } from './services'; 7 | import { PaymentController } from './rest/controllers'; 8 | import { PaymentMutationResolver } from './graphql/mutations'; 9 | import { PaymentQueryResolver } from './graphql/queries'; 10 | 11 | @Module({ 12 | imports: [ 13 | ClientsModule.registerAsync([ 14 | createClientAsyncOptions('auth'), 15 | createClientAsyncOptions('payment'), 16 | createClientAsyncOptions('booking'), 17 | ]), 18 | BookingModule, 19 | ], 20 | controllers: [PaymentController], 21 | providers: [PaymentService, PaymentQueryResolver, PaymentMutationResolver], 22 | }) 23 | export class PaymentModule {} 24 | -------------------------------------------------------------------------------- /src/microservices/payment/rest/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './payment.controller'; 2 | -------------------------------------------------------------------------------- /src/microservices/payment/rest/controllers/payment.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { createClientAsyncOptions } from '@utils/client'; 5 | import { UtilsModule } from '@utils/utils.module'; 6 | import { BookingModule } from '@booking/booking.module'; 7 | import { PaymentService } from '../../services'; 8 | import { PaymentController } from './payment.controller'; 9 | 10 | describe('PaymentController', () => { 11 | let controller: PaymentController; 12 | 13 | beforeEach(async () => { 14 | const module: TestingModule = await Test.createTestingModule({ 15 | imports: [ 16 | ClientsModule.registerAsync([ 17 | createClientAsyncOptions('auth'), 18 | createClientAsyncOptions('payment'), 19 | createClientAsyncOptions('booking'), 20 | ]), 21 | UtilsModule, 22 | BookingModule, 23 | ], 24 | controllers: [PaymentController], 25 | providers: [PaymentService], 26 | }).compile(); 27 | 28 | controller = module.get(PaymentController); 29 | }); 30 | 31 | it('should be defined', () => { 32 | expect(controller).toBeDefined(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/microservices/payment/rest/controllers/payment.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Param, 4 | Get, 5 | UseGuards, 6 | Post, 7 | Put, 8 | Body, 9 | UsePipes, 10 | ValidationPipe, 11 | ParseIntPipe, 12 | } from '@nestjs/common'; 13 | 14 | import { AccessControlGuard } from '@auth/guards'; 15 | import { PaymentService } from '../../services'; 16 | import { RestPayment } from '../models'; 17 | import { CreatePaymentDto } from '../dto'; 18 | 19 | @Controller('payment') 20 | export class PaymentController { 21 | constructor(private readonly paymentService: PaymentService) {} 22 | 23 | @UseGuards(AccessControlGuard) 24 | @Get('/:id') 25 | async getPaymentById( 26 | @Param('id', ParseIntPipe) id: number, 27 | ): Promise { 28 | return this.paymentService.getPaymentById(id); 29 | } 30 | 31 | @UsePipes(new ValidationPipe()) 32 | @UseGuards(AccessControlGuard) 33 | @Post('/') 34 | async createPayment( 35 | @Body() newPayment: CreatePaymentDto, 36 | ): Promise { 37 | return this.paymentService.createPayment(newPayment); 38 | } 39 | 40 | @UsePipes(new ValidationPipe()) 41 | @UseGuards(AccessControlGuard) 42 | @Put('/:id') 43 | async updatePayment( 44 | @Param('id', ParseIntPipe) id: number, 45 | @Body() updatedPayment: CreatePaymentDto, 46 | ): Promise { 47 | return this.paymentService.updatePayment(id, updatedPayment); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/microservices/payment/rest/dto/create-payment.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsPositive, IsString, Min } from 'class-validator'; 2 | 3 | export class CreatePaymentDto { 4 | @IsPositive() 5 | booking_id: number; 6 | 7 | @IsString() 8 | card_token: string; 9 | 10 | @Min(200) 11 | amount: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/microservices/payment/rest/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-payment.dto'; 2 | -------------------------------------------------------------------------------- /src/microservices/payment/rest/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './payment-rest'; 2 | -------------------------------------------------------------------------------- /src/microservices/payment/rest/models/payment-rest.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsInt, IsNumber } from 'class-validator'; 2 | 3 | export class RestPayment { 4 | @IsInt() 5 | id: number; 6 | 7 | @IsDate() 8 | date: Date; 9 | 10 | @IsNumber() 11 | booking_id: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/microservices/payment/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './payment.service'; 2 | -------------------------------------------------------------------------------- /src/microservices/payment/services/payment.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientsModule } from '@nestjs/microservices'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | 4 | import { UtilsModule } from '@utils/utils.module'; 5 | import { createClientAsyncOptions } from '@utils/client'; 6 | import { BookingModule } from '@booking/booking.module'; 7 | import { PaymentService } from './payment.service'; 8 | 9 | describe('PaymentService', () => { 10 | let service: PaymentService; 11 | 12 | beforeEach(async () => { 13 | const module: TestingModule = await Test.createTestingModule({ 14 | imports: [ 15 | ClientsModule.registerAsync([ 16 | createClientAsyncOptions('payment'), 17 | createClientAsyncOptions('booking'), 18 | ]), 19 | UtilsModule, 20 | BookingModule, 21 | ], 22 | providers: [PaymentService], 23 | }).compile(); 24 | 25 | service = module.get(PaymentService); 26 | }); 27 | 28 | it('should be defined', () => { 29 | expect(service).toBeDefined(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/microservices/payment/services/payment.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { ClientProxy } from '@nestjs/microservices'; 3 | 4 | import { ClientService } from '@utils/client'; 5 | import { BookingService } from '@booking/services'; 6 | import { CreatePaymentInput } from '../graphql/input'; 7 | import { GqlPayment } from '../graphql/models'; 8 | import { CreatePaymentDto } from '../rest/dto'; 9 | import { RestPayment } from '../rest/models'; 10 | 11 | @Injectable() 12 | export class PaymentService { 13 | constructor( 14 | @Inject('PAYMENT_MICROSERVICE') 15 | public readonly paymentClient: ClientProxy, 16 | private readonly clientService: ClientService, 17 | private readonly bookingService: BookingService, 18 | ) {} 19 | 20 | async getPaymentById(id: number): Promise { 21 | return this.clientService.sendMessageWithPayload( 22 | this.paymentClient, 23 | { role: 'payment', cmd: 'get' }, 24 | id, 25 | ); 26 | } 27 | 28 | async createPayment( 29 | newPayment: CreatePaymentDto | CreatePaymentInput, 30 | ): Promise { 31 | const booking = this.bookingService.getBookingById(newPayment.booking_id); 32 | 33 | return this.clientService.sendMessageWithPayload( 34 | this.paymentClient, 35 | { role: 'payment', cmd: 'create' }, 36 | { newPayment, booking }, 37 | ); 38 | } 39 | 40 | async updatePayment( 41 | id: number, 42 | updatedPayment: CreatePaymentDto | CreatePaymentInput, 43 | ): Promise { 44 | return this.clientService.sendMessageWithPayload( 45 | this.paymentClient, 46 | { role: 'payment', cmd: 'update' }, 47 | { id, updatedPayment }, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/open-id/README.md: -------------------------------------------------------------------------------- 1 | # OpenID 2 | 3 | Open ID Connect module. Grants OAuth with popular Social Media Providers like Facebook, Gmail, or Github 4 | 5 | This directory contains: 6 | 7 | - `controllers` directory with open-id controllers 8 | - `guards` directory with OpenIdGuard 9 | - `helpers` directory with buildIdClient helper method 10 | - `services` directory with open-id services 11 | - `strategies` directory with open-id strategy and strategy factory 12 | -------------------------------------------------------------------------------- /src/open-id/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './open-id.controller'; 2 | -------------------------------------------------------------------------------- /src/open-id/controllers/open-id.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { OpenIdService } from '../services'; 4 | import { OpenIdController } from './open-id.controller'; 5 | 6 | describe('OpenIdController', () => { 7 | let controller: OpenIdController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [OpenIdController], 12 | providers: [OpenIdService] 13 | }).compile(); 14 | 15 | controller = module.get(OpenIdController); 16 | }); 17 | 18 | it('should be defined', () => { 19 | expect(controller).toBeDefined(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/open-id/controllers/open-id.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | import { OpenIdGuard } from '../guards'; 4 | 5 | import { OpenIdService } from '../services'; 6 | 7 | @Controller('open-id') 8 | export class OpenIdController { 9 | constructor(private readonly openIdService: OpenIdService) {} 10 | 11 | @UseGuards(OpenIdGuard) 12 | @Get('/login') 13 | login() {} 14 | 15 | @Get('/user') 16 | user(@Req() req) { 17 | return req.user; 18 | } 19 | 20 | @UseGuards(OpenIdGuard) 21 | @Get('/callback') 22 | loginCallback(@Res() res: Response) { 23 | return res.redirect('/api/open-id/user'); 24 | } 25 | 26 | @Get('/logout') 27 | async logout(@Req() req, @Res() res: Response) { 28 | return this.openIdService.logout(req, res); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/open-id/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './open-id.guard'; 2 | -------------------------------------------------------------------------------- /src/open-id/guards/open-id.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class OpenIdGuard extends AuthGuard('open-id') { 6 | async canActivate(context: ExecutionContext) { 7 | const result = (await super.canActivate(context)) as boolean; 8 | const request = context.switchToHttp().getRequest(); 9 | await super.logIn(request); 10 | return result; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/open-id/helpers/build-open-id-client.ts: -------------------------------------------------------------------------------- 1 | import { Issuer } from 'openid-client'; 2 | 3 | export const buildOpenIdClient = async () => { 4 | const TrustIssuer = await Issuer.discover( 5 | `${process.env.OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER}/.well-known/openid-configuration`, 6 | ); 7 | const client = new TrustIssuer.Client({ 8 | client_id: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_CLIENT_ID, 9 | client_secret: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_CLIENT_SECRET, 10 | }); 11 | return client; 12 | }; 13 | -------------------------------------------------------------------------------- /src/open-id/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './build-open-id-client'; 2 | -------------------------------------------------------------------------------- /src/open-id/open-id.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { PassportModule } from '@nestjs/passport'; 4 | 5 | import { SessionSerializer } from '@security/serializers'; 6 | import { OpenIdController } from './controllers'; 7 | import { OpenIdService } from './services'; 8 | import { OpenIdStrategyFactory } from './strategies'; 9 | 10 | @Module({ 11 | imports: [PassportModule, JwtModule.register({})], 12 | controllers: [OpenIdController], 13 | providers: [OpenIdService, OpenIdStrategyFactory, SessionSerializer], 14 | }) 15 | export class OpenIdModule {} 16 | -------------------------------------------------------------------------------- /src/open-id/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './open-id.service'; 2 | -------------------------------------------------------------------------------- /src/open-id/services/open-id.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { OpenIdService } from './open-id.service'; 3 | 4 | describe('OpenIdService', () => { 5 | let service: OpenIdService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [OpenIdService], 10 | }).compile(); 11 | 12 | service = module.get(OpenIdService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/open-id/services/open-id.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | import { Issuer } from 'openid-client'; 4 | 5 | @Injectable() 6 | export class OpenIdService { 7 | async logout(req, res: Response) { 8 | const id_token = req.user ? req.user.id_token : undefined; 9 | req.logout(); 10 | req.session.destroy(async (error: any) => { 11 | const TrustIssuer = await Issuer.discover( 12 | `${process.env.OAUTH2_CLIENT_PROVIDER_GOOGLE_ISSUER}/.well-known/openid-configuration`, 13 | ); 14 | const end_session_endpoint = TrustIssuer.metadata.end_session_endpoint; 15 | if (end_session_endpoint) { 16 | const idTokenHint = id_token ? '&id_token_hint=' + id_token : ''; 17 | const redirectUrl = `${end_session_endpoint}?post_logout_redirect_uri=${process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_POST_LOGOUT_REDIRECT_URI}${idTokenHint}`; 18 | 19 | res.redirect(redirectUrl); 20 | } else { 21 | res.redirect('/'); 22 | } 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/open-id/strategies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './open-id-strategy-factory'; 2 | export * from './open-id.strategy'; 3 | -------------------------------------------------------------------------------- /src/open-id/strategies/open-id-strategy-factory.ts: -------------------------------------------------------------------------------- 1 | import { buildOpenIdClient } from '../helpers'; 2 | import { OpenIdService } from '../services'; 3 | import { OpenIdStrategy } from './open-id.strategy'; 4 | 5 | export const OpenIdStrategyFactory = { 6 | provide: 'OpenIdStrategy', 7 | inject: [OpenIdService], 8 | useFactory: async (openIdService: OpenIdService) => { 9 | const client = await buildOpenIdClient(); 10 | const strategy = new OpenIdStrategy(openIdService, client); 11 | return strategy; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/open-id/strategies/open-id.strategy.ts: -------------------------------------------------------------------------------- 1 | import { UnauthorizedException } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Strategy, Client, TokenSet, UserinfoResponse } from 'openid-client'; 4 | 5 | import { OpenIdService } from '../services'; 6 | 7 | export class OpenIdStrategy extends PassportStrategy(Strategy, 'openid') { 8 | client: Client; 9 | 10 | constructor(private readonly openIdService: OpenIdService, client: Client) { 11 | super({ 12 | client: client, 13 | params: { 14 | redirect_uri: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_REDIRECT_URI, 15 | scope: process.env.OAUTH2_CLIENT_REGISTRATION_LOGIN_SCOPE, 16 | }, 17 | passReqToCallback: false, 18 | usePKCE: false, 19 | }); 20 | 21 | this.client = client; 22 | } 23 | 24 | async validate(tokenset: TokenSet): Promise { 25 | try { 26 | const userInfo: UserinfoResponse = await this.client.userinfo(tokenset); 27 | const { id_token, access_token, refresh_token } = tokenset; 28 | 29 | const user = { 30 | id_token: id_token, 31 | access_token: access_token, 32 | refresh_token: refresh_token, 33 | userInfo, 34 | }; 35 | 36 | return user; 37 | } catch (error) { 38 | throw new UnauthorizedException('Cannot validate OpenID'); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/queues/README.md: -------------------------------------------------------------------------------- 1 | # Queue 2 | 3 | Bull Queue Wrapper Module. 4 | 5 | This directory contains: 6 | 7 | - BullQueueModule which is a wrapper for BullModule 8 | - `configs` directory with bull and queue async config and index.ts exporting them 9 | - `services` directory with bull queue service to handle queue generation 10 | -------------------------------------------------------------------------------- /src/queues/bull-queue.module.ts: -------------------------------------------------------------------------------- 1 | import { BullModule } from '@nestjs/bull'; 2 | import { Global, Module } from '@nestjs/common'; 3 | 4 | import { BullQueueService } from './services'; 5 | import { bullAsyncConfig, queueAsyncConfig } from './configs'; 6 | 7 | @Global() 8 | @Module({ 9 | imports: [ 10 | BullModule.forRootAsync(bullAsyncConfig), 11 | BullModule.registerQueueAsync(queueAsyncConfig), 12 | ], 13 | providers: [BullQueueService], 14 | }) 15 | export class BullQueueModule {} 16 | -------------------------------------------------------------------------------- /src/queues/configs/bull-async-config.ts: -------------------------------------------------------------------------------- 1 | import { SharedBullAsyncConfiguration } from '@nestjs/bull'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | 4 | export const bullAsyncConfig: SharedBullAsyncConfiguration = { 5 | imports: [ConfigModule], 6 | inject: [ConfigService], 7 | useFactory: (configService: ConfigService) => ({ 8 | redis: { 9 | host: configService.get('REDIS_HOST'), 10 | port: configService.get('REDIS_PORT'), 11 | }, 12 | prefix: configService.get('BULL_PREFIX'), 13 | limiter: { 14 | max: configService.get('BULL_MAX_JOBS'), 15 | duration: configService.get( 16 | 'BULL_MAX_DURATION_FOR_JOB_IN_MILISECONDS', 17 | ), 18 | }, 19 | }), 20 | }; 21 | -------------------------------------------------------------------------------- /src/queues/configs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bull-async-config'; 2 | export * from './queue-async-config'; 3 | -------------------------------------------------------------------------------- /src/queues/configs/queue-async-config.ts: -------------------------------------------------------------------------------- 1 | import { BullModuleAsyncOptions } from '@nestjs/bull'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | 4 | export const queueAsyncConfig: BullModuleAsyncOptions = { 5 | imports: [ConfigModule], 6 | inject: [ConfigService], 7 | useFactory: (configService: ConfigService) => ({ 8 | name: configService.get('QUEUE_NAME'), 9 | }), 10 | }; 11 | -------------------------------------------------------------------------------- /src/queues/services/bull-queue.service.ts: -------------------------------------------------------------------------------- 1 | import { InjectQueue } from '@nestjs/bull'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { JobOptions, Queue } from 'bull'; 4 | 5 | @Injectable() 6 | export class BullQueueService { 7 | constructor( 8 | @InjectQueue(process.env.QUEUE_NAME) 9 | private readonly queue: Queue, 10 | ) {} 11 | 12 | async addJobToQueue( 13 | jobName: string, 14 | customData?: any, 15 | customOptions?: JobOptions, 16 | ) { 17 | await this.queue.add(jobName, customData, customOptions); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/queues/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bull-queue.service'; 2 | -------------------------------------------------------------------------------- /src/security/README.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | Things related to application security. 4 | 5 | This directory contains: 6 | 7 | - `configs` directory with security libraries configs 8 | - `guards` directory with global security guards 9 | - `middlewares` directory with global middlewares 10 | -------------------------------------------------------------------------------- /src/security/configs/csurfConfigOptions.ts: -------------------------------------------------------------------------------- 1 | export const csurfConfigOptions = { 2 | cookie: { 3 | key: '_csrf', 4 | sameSite: true, 5 | httpOnly: true, 6 | secure: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/security/configs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rateLimitConfig'; 2 | export * from './redisSessionConfig'; 3 | export * from './csurfConfigOptions'; 4 | -------------------------------------------------------------------------------- /src/security/configs/rateLimitConfig.ts: -------------------------------------------------------------------------------- 1 | const quarterOfAnHour: number = 15 * 60 * 1000; 2 | const numberOfRequestsBeforeBan: number = 100; 3 | const returnedMessage: string = 'Too many requests sent from this IP Address'; 4 | 5 | export const rateLimitConfigObject = { 6 | windowMs: quarterOfAnHour, 7 | max: numberOfRequestsBeforeBan, 8 | message: returnedMessage, 9 | }; 10 | -------------------------------------------------------------------------------- /src/security/configs/redisSessionConfig.ts: -------------------------------------------------------------------------------- 1 | import * as connectRedis from 'connect-redis'; 2 | import * as session from 'express-session'; 3 | import * as redis from 'redis'; 4 | 5 | const redisStore = connectRedis(session); 6 | const RedisClient = redis.createClient({ 7 | host: process.env.REDIS_HOST, 8 | port: process.env.REDIS_PORT, 9 | }); 10 | RedisClient.auth(process.env.REDIS_PASSWORD); 11 | 12 | const redisSessionConfig = { 13 | store: new redisStore({ client: RedisClient }), 14 | secret: process.env.SESSION_SECRET, 15 | resave: false, // will default to false in near future: https://github.com/expressjs/session#resave 16 | saveUninitialized: false, // will default to false in near future: https://github.com/expressjs/session#saveuninitialized 17 | rolling: true, // keep session alive 18 | cookie: { 19 | maxAge: 30 * 60 * 1000, // session expires in 1hr, refreshed by `rolling: true` option. 20 | httpOnly: true, // so that cookie can't be accessed via client-side script 21 | }, 22 | }; 23 | 24 | export const createRedisSession = () => { 25 | return session(redisSessionConfig); 26 | }; 27 | -------------------------------------------------------------------------------- /src/security/guards/frontend-cookie.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { FrontendCookieGuard } from './frontend-cookie.guard'; 2 | 3 | describe('FrontendCookieGuard', () => { 4 | it('should be defined', () => { 5 | expect(new FrontendCookieGuard()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/security/guards/frontend-cookie.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { Request } from 'express'; 8 | import { Observable } from 'rxjs'; 9 | 10 | @Injectable() 11 | export class FrontendCookieGuard implements CanActivate { 12 | canActivate( 13 | context: ExecutionContext, 14 | ): boolean | Promise | Observable { 15 | let req: Request; 16 | req = context.getArgs()[context.getArgs().length - 2].req; 17 | if (!req) { 18 | req = context.switchToHttp().getRequest(); 19 | } 20 | 21 | if (req.cookies.frontend_cookie !== process.env.FRONTEND_COOKIE) 22 | throw new UnauthorizedException('You cannot access this resource'); 23 | 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/security/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './frontend-cookie.guard'; 2 | -------------------------------------------------------------------------------- /src/security/middlewares/csrf.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | 3 | // This array should be filled with all routes that will be processing HTML forms 4 | //(i.e. creating new content, changing password) 5 | const includedRoutes = ['change-user-password']; 6 | 7 | /** 8 | * A middleware wrapper for CSRF (csurf) middleware. 9 | * Checks if a current route should be validated with CSRF Token or handled normally. 10 | * Uses `includedRoutes` array to check the routes 11 | */ 12 | export const csrfMiddleware = ( 13 | req: Request, 14 | res: Response, 15 | next: NextFunction, 16 | csrf, 17 | ) => { 18 | const includedRoute = includedRoutes.find((route) => req.url.includes(route)); 19 | 20 | if (!includedRoute) return next(); 21 | 22 | csrf(req, res, next); 23 | }; 24 | -------------------------------------------------------------------------------- /src/security/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './csrf.middleware'; 2 | -------------------------------------------------------------------------------- /src/security/serializers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './session.serializer'; 2 | -------------------------------------------------------------------------------- /src/security/serializers/session.serializer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportSerializer } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class SessionSerializer extends PassportSerializer { 6 | serializeUser(user: any, done: (err: Error, user: any) => void): any { 7 | done(null, user); 8 | } 9 | 10 | deserializeUser( 11 | payload: any, 12 | done: (err: Error, payload: string) => void, 13 | ): any { 14 | done(null, payload); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/README.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | UtilsModule. Useful utilities that can be used across the application 4 | 5 | This directory contains: 6 | 7 | - UtilsModule 8 | - `axios` directory with AxiosWrapperModule which is a wrapper for HttpModule 9 | - `client` directory with ClientService that helps with handling microservice requests 10 | -------------------------------------------------------------------------------- /src/utils/axios/axios-wrapper.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, Module } from '@nestjs/common'; 2 | 3 | import { axiosAsyncConfig } from './config'; 4 | 5 | @Module({ 6 | imports: [HttpModule.registerAsync(axiosAsyncConfig)], 7 | }) 8 | export class AxiosWrapperModule {} 9 | -------------------------------------------------------------------------------- /src/utils/axios/config/axios-async-config.ts: -------------------------------------------------------------------------------- 1 | import { HttpModuleAsyncOptions } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | 4 | export const axiosAsyncConfig: HttpModuleAsyncOptions = { 5 | imports: [ConfigModule], 6 | inject: [ConfigService], 7 | useFactory: async (configService: ConfigService) => ({ 8 | timeout: configService.get('AXIOS_TIMEOUT'), 9 | maxRedirects: configService.get('AXIOS_MAX_REDIRECTS'), 10 | }), 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/axios/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './axios-async-config'; 2 | -------------------------------------------------------------------------------- /src/utils/client/client.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable } from '@nestjs/common'; 2 | import { ClientProxy } from '@nestjs/microservices'; 3 | 4 | import { IMessagePattern } from './interfaces'; 5 | 6 | @Injectable() 7 | export class ClientService { 8 | /** 9 | * Method used to send the request to the corresponding microservice. It accepts following parameters: 10 | * 11 | * - @param {ClientProxy} client - microservice client to send the message to 12 | * - @param {IMessagePattern} messagePattern - object containing the pattern for a message (i.e. `{ role: 'user', cmd: 'create' }`) 13 | * - @param {*} payload - data to send to the microservice client 14 | * 15 | * @return {*} {Promise} - returned response from a microservice or an adequate HTTP exception 16 | */ 17 | async sendMessageWithPayload( 18 | client: ClientProxy, 19 | messagePattern: IMessagePattern, 20 | payload: any, 21 | ): Promise { 22 | try { 23 | return await client.send(messagePattern, payload).toPromise(); 24 | } catch (error) { 25 | throw new HttpException(error.errorStatus, error.statusCode); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/client/config/client-async-options.ts: -------------------------------------------------------------------------------- 1 | import { Transport, ClientsProviderAsyncOptions } from '@nestjs/microservices'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | 4 | /** 5 | * Method that creates a microservice option object used for connecting to another client microservice. 6 | * 7 | * @param {string} microserviceName - name of a microservice to connect to 8 | * 9 | * @return {*} {ClientsProviderAsyncOptions} - connection options for a microservice client 10 | */ 11 | export const createClientAsyncOptions = ( 12 | microserviceName: string, 13 | ): ClientsProviderAsyncOptions => { 14 | const upperCaseMicroserviceName = microserviceName.toUpperCase(); 15 | 16 | const clientAsyncOptions: ClientsProviderAsyncOptions = { 17 | name: `${upperCaseMicroserviceName}_MICROSERVICE`, 18 | imports: [ConfigModule], 19 | inject: [ConfigService], 20 | useFactory: (configService: ConfigService) => ({ 21 | transport: Transport.RMQ, 22 | options: { 23 | urls: [ 24 | `amqp://${configService.get( 25 | 'RABBITMQ_DEFAULT_USER', 26 | )}:${configService.get('RABBITMQ_DEFAULT_PASS')}@${configService.get( 27 | 'RABBITMQ_NODENAME', 28 | )}:${configService.get( 29 | 'RABBITMQ_FIRST_HOST_PORT', 30 | )}/${configService.get('RABBITMQ_DEFAULT_VHOST')}`, 31 | ], 32 | queue: `${microserviceName}_queue`, 33 | queueOptions: { 34 | durable: false, 35 | }, 36 | }, 37 | }), 38 | }; 39 | 40 | return clientAsyncOptions; 41 | }; 42 | -------------------------------------------------------------------------------- /src/utils/client/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client-async-options'; 2 | -------------------------------------------------------------------------------- /src/utils/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client.service'; 2 | export * from './config'; 3 | export * from './interfaces'; 4 | -------------------------------------------------------------------------------- /src/utils/client/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './message-pattern.interface'; 2 | -------------------------------------------------------------------------------- /src/utils/client/interfaces/message-pattern.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for message pattern used to send request to microservice 3 | * 4 | * - @property {string} role 5 | * - @property {string} cmd 6 | * - @property {*} metadata? 7 | * 8 | */ 9 | export interface IMessagePattern { 10 | role: string; 11 | cmd: string; 12 | metadata?: any; 13 | type?: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/utils.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { AxiosWrapperModule } from './axios/axios-wrapper.module'; 4 | import { ClientService } from './client'; 5 | 6 | @Global() 7 | @Module({ 8 | imports: [AxiosWrapperModule], 9 | providers: [ClientService], 10 | exports: [ClientService], 11 | }) 12 | export class UtilsModule {} 13 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = 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", "test", "dist", "**/*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 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "moduleResolution": "node", 15 | "paths": { 16 | "@infrastructure/*": ["src/item/infrastructure/*"], 17 | "@auth/*": ["src/microservices/auth/*"], 18 | "@booking/*": ["src/microservices/booking/*"], 19 | "@catalog/*": ["src/microservices/catalog/*"], 20 | "@customer/*": ["src/microservices/customer/*"], 21 | "@mail/*": ["src/microservices/mail/*"], 22 | "@utils/*": ["src/utils/*"], 23 | "@test/*": ["test/*"], 24 | "@security/*": ["src/security/*"], 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /webpack-hmr.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | const StartServerPlugin = require('start-server-webpack-plugin'); 4 | 5 | module.exports = function(options) { 6 | return { 7 | ...options, 8 | entry: ['webpack/hot/poll?100', options.entry], 9 | watch: true, 10 | externals: [ 11 | nodeExternals({ 12 | allowlist: ['webpack/hot/poll?100'], 13 | }), 14 | ], 15 | plugins: [ 16 | ...options.plugins, 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.WatchIgnorePlugin([/\.js$/, /\.d\.ts$/]), 19 | new StartServerPlugin({ name: options.output.filename }), 20 | ], 21 | }; 22 | }; --------------------------------------------------------------------------------