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