├── .commitlintrc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── codeql
│ └── codeql-configuration.yml
├── dependabot.yml
├── pull_request_template.md
└── workflows
│ ├── ci.yml
│ ├── codeql-analysis.yml
│ └── update-package-lock.yaml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .lintstagedrc
├── LICENSE
├── README.md
├── config
└── .env
├── docker-compose.yml
├── docker
├── docker-compose.prod.yml
└── docker-compose.staging.yml
├── init.sql
├── multitenancy-rest-service
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── Dockerfile
├── README.md
├── config
│ ├── .env
│ ├── prod.env
│ └── staging.env
├── nest-cli.json
├── package.json
├── src
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── auth
│ │ ├── auth.service.ts
│ │ ├── cache.publicKey.ts
│ │ ├── guards
│ │ │ └── keycloak-auth.guard.ts
│ │ ├── permission.decorator.ts
│ │ └── roles.decorator.ts
│ ├── config
│ │ ├── app.ts
│ │ ├── client.ts
│ │ ├── index.ts
│ │ ├── keycloak.ts
│ │ └── mail.server.ts
│ ├── dto
│ │ ├── client.dto.ts
│ │ ├── column.dto.ts
│ │ ├── create.realm.dto.ts
│ │ ├── create.role.dto.ts
│ │ ├── credentials.dto.ts
│ │ ├── db.details.dto.ts
│ │ ├── delete.permission.dto.ts
│ │ ├── delete.role.dto.ts
│ │ ├── getpermissions.dto.ts
│ │ ├── index.ts
│ │ ├── logout.body.dto.ts
│ │ ├── permission.dto.ts
│ │ ├── policy.dto.ts
│ │ ├── provision.tenant.table.dto.ts
│ │ ├── realm.dto.ts
│ │ ├── refresh.access-token.dto.ts
│ │ ├── register.tenant.dto.ts
│ │ ├── resource.dto.ts
│ │ ├── role.getinfo.dto.ts
│ │ ├── scope.dto.ts
│ │ ├── scope.representation.dto.ts
│ │ ├── tenant.adminuser.dto.ts
│ │ ├── tenant.user.dto.ts
│ │ ├── update.permission.dto.ts
│ │ ├── update.role.dto.ts
│ │ ├── update.tenant.dto .ts
│ │ ├── user.delete.dto.ts
│ │ ├── user.details.dto.ts
│ │ ├── user.getinfo.dto.ts
│ │ ├── user.update.dto.ts
│ │ └── users.query.dto.ts
│ ├── filter
│ │ └── http-exception.filter.ts
│ ├── iam
│ │ ├── client.ts
│ │ ├── index.ts
│ │ ├── keycloak.ts
│ │ ├── keycloakRealm.ts
│ │ ├── keycloakUser.ts
│ │ ├── permission.ts
│ │ ├── policy.ts
│ │ ├── resource.ts
│ │ └── scope.ts
│ ├── logger
│ │ ├── index.ts
│ │ ├── logger.middleware.ts
│ │ └── system.logger.ts
│ ├── main.ts
│ ├── swagger
│ │ └── index.ts
│ └── utils
│ │ ├── connection.utils.ts
│ │ ├── cors.ts
│ │ ├── enums
│ │ ├── index.ts
│ │ ├── permission.enums.ts
│ │ └── roles.enums.ts
│ │ ├── httpclient.ts
│ │ ├── index.ts
│ │ └── interfaces
│ │ └── httpclient.ts
├── test
│ └── unit
│ │ ├── app.controller.spec.ts
│ │ ├── app.service.spec.ts
│ │ ├── auth.service.spec.ts
│ │ ├── cache.publicKey.spec.ts
│ │ ├── client.spec.ts
│ │ ├── keycloak-auth.guard.spec.ts
│ │ ├── keycloak-realm.spec.ts
│ │ ├── keycloak-user.spec.ts
│ │ ├── permission.spec.ts
│ │ ├── policy.spec.ts
│ │ ├── resource.spec.ts
│ │ └── scope.spec.ts
├── tsconfig-paths-bootstrap.js
├── tsconfig.build.json
└── tsconfig.json
├── package-lock.json
├── package.json
├── sonar-project.properties
├── tenant-config-service
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── Dockerfile
├── README.md
├── config
│ ├── .env
│ ├── prod.env
│ └── staging.env
├── nest-cli.json
├── package.json
├── src
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.ts
│ ├── tenant-config
│ │ ├── config
│ │ │ ├── database.ts
│ │ │ ├── index.ts
│ │ │ └── micro-service.ts
│ │ ├── db
│ │ │ └── database.module.ts
│ │ ├── dto
│ │ │ └── tenant.config.dto.ts
│ │ ├── entities
│ │ │ └── tenant.entity.ts
│ │ ├── tenant.config.controller.ts
│ │ ├── tenant.config.module.ts
│ │ └── tenant.config.service.ts
│ └── transport
│ │ └── transport.ts
├── test
│ └── unit
│ │ ├── tenant.config.controller.spec.ts
│ │ └── tenant.config.service.spec.ts
├── tsconfig-paths-bootstrap.js
├── tsconfig.build.json
└── tsconfig.json
├── tenant-master-service
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── Dockerfile
├── README.md
├── config
│ ├── .env
│ ├── prod.env
│ └── staging.env
├── nest-cli.json
├── package.json
├── src
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.ts
│ ├── tenant-master
│ │ ├── config
│ │ │ ├── database.ts
│ │ │ ├── index.ts
│ │ │ └── micro-service.ts
│ │ ├── dto
│ │ │ └── tenant.details.dto.ts
│ │ ├── tenant.master.controller.ts
│ │ ├── tenant.master.module.ts
│ │ └── tenant.master.service.ts
│ └── transport
│ │ └── transport.ts
├── test
│ └── unit
│ │ ├── tenant.master.controller.spec.ts
│ │ └── tenant.master.service.spec.ts
├── tsconfig-paths-bootstrap.js
├── tsconfig.build.json
└── tsconfig.json
├── tenant-provisioning
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── Dockerfile
├── README.md
├── config
│ ├── .env
│ ├── prod.env
│ └── staging.env
├── nest-cli.json
├── package.json
├── src
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.ts
│ ├── tenant-provisioning
│ │ ├── config
│ │ │ ├── database.ts
│ │ │ ├── index.ts
│ │ │ ├── micro-service.ts
│ │ │ └── tenantdb.ts
│ │ ├── connection.utils.ts
│ │ ├── dto
│ │ │ ├── column.dto.ts
│ │ │ ├── provision.tenant.dto.ts
│ │ │ ├── provision.tenant.table.dto.ts
│ │ │ └── seeding-data.dto.ts
│ │ ├── scripts
│ │ │ ├── create-database.sql
│ │ │ ├── create-table.sql
│ │ │ ├── ping.sql
│ │ │ └── seed-data.sql
│ │ ├── tenantprovision.controller.ts
│ │ ├── tenantprovision.module.ts
│ │ └── tenantprovision.service.ts
│ └── transport
│ │ └── transport.ts
├── test
│ └── unit
│ │ ├── tenantprovision.controller.spec.ts
│ │ └── tenantprovision.service.spec.ts
├── tsconfig-paths-bootstrap.js
├── tsconfig.build.json
└── tsconfig.json
├── tenant-registration
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── Dockerfile
├── README.md
├── config
│ ├── .env
│ ├── prod.env
│ └── staging.env
├── nest-cli.json
├── package.json
├── src
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.ts
│ ├── tenant-reg
│ │ ├── config
│ │ │ ├── database.ts
│ │ │ ├── index.ts
│ │ │ └── micro-service.ts
│ │ ├── db
│ │ │ └── database.module.ts
│ │ ├── dto
│ │ │ ├── identify.tenant.dto.ts
│ │ │ ├── register.tenant.dto.ts
│ │ │ └── tenant.details.dto.ts
│ │ ├── entity
│ │ │ └── tenant.entity.ts
│ │ ├── identifier
│ │ │ └── identifier.service.ts
│ │ ├── registertenant.controller.ts
│ │ ├── registertenant.module.ts
│ │ ├── registertenant.service.ts
│ │ └── utils
│ │ │ └── bcrypt.ts
│ └── transport
│ │ └── transport.ts
├── test
│ └── unit
│ │ ├── registertenant.controller.spec.ts
│ │ └── registertenant.service.spec.ts
├── tsconfig-paths-bootstrap.js
├── tsconfig.build.json
└── tsconfig.json
└── wiki
├── docs
├── api.md
├── auth.md
├── communication.md
├── contribution.md
├── contribution
│ ├── bug-reports.md
│ ├── feature-requests.md
│ └── pull-requests.md
├── coverage.md
├── keycloak.md
├── microservices
│ ├── tenant-config-service.md
│ ├── tenant-master-service.md
│ ├── tenant-provisioning.md
│ └── tenant-registration.md
├── miscellaneous
│ ├── clean-docker.md
│ ├── git-branch.md
│ ├── git-commits.md
│ └── installation-pretteri-husky-lint.md
├── multitenancy-rest-service.md
└── vm.md
└── images
├── Token-Validation-Method-I.png
├── Token-validation-Method-II.png
├── microservices-communication.png
├── single-vs-multi-tenant.png
└── swagger.PNG
/.commitlintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"]
3 | }
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/codeql/codeql-configuration.yml:
--------------------------------------------------------------------------------
1 | name : CodeQL Configuration
2 |
3 | paths:
4 | - './multitenancy-rest-service/src'
5 | - './tenant-config-service/src'
6 | - './tenant-provisioning/src'
7 | - './tenant-registration/src'
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | - package-ecosystem: npm
8 | directory: "/"
9 | schedule:
10 | interval: "weekly"
11 | open-pull-requests-limit: 10
12 | target-branch: "dev"
13 | # Increase the version requirements
14 | # only when required
15 | versioning-strategy: increase-if-necessary
16 | labels:
17 | - "dependencies"
18 | - "npm"
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## PR Checklist
2 | Please check if your PR fulfills the following requirements:
3 |
4 | - [ ] The commit message follows our guidelines: https://github.com/NeoSOFT-Technologies/rest-node-nestjs/blob/main/CONTRIBUTING.md
5 | - [ ] Tests for the changes have been added (for bug fixes / features)
6 | - [ ] Docs have been added / updated (for bug fixes / features)
7 |
8 |
9 | ## PR Type
10 | What kind of change does this PR introduce?
11 |
12 |
13 | - [ ] Bugfix
14 | - [ ] Feature
15 | - [ ] Code style update (formatting, local variables)
16 | - [ ] Refactoring (no functional changes, no api changes)
17 | - [ ] Build related changes
18 | - [ ] CI related changes
19 | - [ ] Other... Please describe:
20 |
21 | ## What is the current behavior?
22 |
23 |
24 | Issue Number: N/A
25 |
26 |
27 | ## What is the new behavior?
28 |
29 |
30 | ## Does this PR introduce a breaking change?
31 | - [ ] Yes
32 | - [ ] No
33 |
34 |
35 |
36 |
37 | ## Other information
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | - dev
7 | - release-*
8 | pull_request:
9 | types: [opened, synchronize, reopened]
10 | branches:
11 | - main
12 | - dev
13 | - release-*
14 | jobs:
15 | CI:
16 | strategy:
17 | matrix:
18 | # node-version: [10.x, 12.x, 14.x]
19 | node-version: [14.x]
20 | runs-on: ubuntu-latest
21 | steps:
22 | - name: Checkout Code
23 | uses: actions/checkout@v2
24 | - name: Set up nodejs version ${{ matrix.node-version }}
25 | uses: actions/setup-node@v3.2.0
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | - name: Install package
29 | run: npm install
30 | - name: Build
31 | run: npm run build --if-present
32 | - name: SonarCloud Scan
33 | uses: sonarsource/sonarcloud-github-action@master
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
--------------------------------------------------------------------------------
/.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:
17 | - main
18 | - dev
19 | - release-*
20 | pull_request:
21 | # The branches below must be a subset of the branches above
22 | branches:
23 | - main
24 | - dev
25 | - release-*
26 | schedule:
27 | - cron: '17 10 * * 2'
28 |
29 | jobs:
30 | analyze:
31 | name: Analyze
32 | runs-on: ubuntu-latest
33 | permissions:
34 | actions: read
35 | contents: read
36 | security-events: write
37 |
38 | strategy:
39 | fail-fast: false
40 | matrix:
41 | language: [ 'javascript' ]
42 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
43 | # Learn more:
44 | # 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
45 |
46 | steps:
47 | - name: Checkout repository
48 | uses: actions/checkout@v2
49 |
50 | # Initializes the CodeQL tools for scanning.
51 | - name: Initialize CodeQL
52 | uses: github/codeql-action/init@v2
53 | with:
54 | config-file: ./.github/codeql/codeql-configuration.yml
55 | languages: ${{ matrix.language }}
56 | # If you wish to specify custom queries, you can do so here or in a config file.
57 | # By default, queries listed here will override any specified in a config file.
58 | # Prefix the list here with "+" to use these queries and those in the config file.
59 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
60 |
61 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
62 | # If this step fails, then you should remove it and run the build manually (see below)
63 | - name: Autobuild
64 | uses: github/codeql-action/autobuild@v2
65 |
66 | # ℹ️ Command-line programs to run using the OS shell.
67 | # 📚 https://git.io/JvXDl
68 |
69 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
70 | # and modify them (or add more) to build your code if your project
71 | # uses a compiled language
72 |
73 | #- run: |
74 | # make bootstrap
75 | # make release
76 |
77 | - name: Perform CodeQL Analysis
78 | uses: github/codeql-action/analyze@v2
79 |
--------------------------------------------------------------------------------
/.github/workflows/update-package-lock.yaml:
--------------------------------------------------------------------------------
1 | name: Update package-lock.json
2 |
3 | on:
4 | schedule:
5 | # This is probably 6am UTC, which is 10pm PST or 11pm PDT
6 | # Alternatively, 6am local is also fine
7 | - cron: '0 6 * * *'
8 | workflow_dispatch: {}
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: actions/setup-node@v3.2.0
17 | with:
18 | node-version: 12
19 | registry-url: https://registry.npmjs.org/
20 |
21 | - name: Configure git and update package-lock.json
22 | run: |
23 | git config user.email "santosh.shinde@neosofttech.com"
24 | git config user.name "NeoSoft Bot"
25 | npm install --package-lock-only
26 | git add -f package-lock.json
27 | if git commit -m "Update package-lock.json"; then
28 | git push
29 | fi
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no -- commitlint --edit
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run precommit
5 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "./tenant-config-service/{src,apps,libs,test}/**/*.ts": ["npm run --prefix tenant-config-service lint"],
3 | "./tenant-master-service/{src,apps,libs,test}/**/*.ts": ["npm run --prefix tenant-master-service lint"],
4 | "./tenant-provisioning/{src,apps,libs,test}/**/*.ts": ["npm run --prefix tenant-provisioning lint"],
5 | "./tenant-registration/{src,apps,libs,test}/**/*.ts": ["npm run --prefix tenant-registration lint"],
6 | "./tenant-config-service/**/*.{ts,js,json,*rc}": ["npm run --prefix tenant-config-service format"],
7 | "./tenant-master-service/**/*.{ts,js,json,*rc}": ["npm run --prefix tenant-master-service format"],
8 | "./tenant-provisioning/**/*.{ts,js,json,*rc}": ["npm run --prefix tenant-provisioning format"],
9 | "./tenant-registration/**/*.{ts,js,json,*rc}": ["npm run --prefix tenant-registration format"]
10 | }
11 |
--------------------------------------------------------------------------------
/config/.env:
--------------------------------------------------------------------------------
1 | DB_HOST=database
2 | DB_DATABASE=rest_api
3 | DB_USER=root
4 | DB_PASSWORD=root
5 | DB_PORT=3306
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | database:
4 | image: 'mysql:8'
5 | cap_add:
6 | - SYS_NICE
7 | container_name: 'rest_api_mysql'
8 | hostname: 'rest_api_mysql'
9 | networks:
10 | - internal
11 | ports:
12 | - '127.0.0.1:3306:3306'
13 | volumes:
14 | - ./init.sql:/docker-entrypoint-initdb.d/init.sql
15 | - mysql:/var/lib/mysql
16 | environment:
17 | MYSQL_EXPOSE_PORT: '3306'
18 | MYSQL_ROOT_PASSWORD: 'root'
19 | MYSQL_USER: 'root'
20 | MYSQL_ROOT_USER: 'root'
21 | MYSQL_PASSWORD: 'root'
22 |
23 | keycloak:
24 | image: quay.io/keycloak/keycloak:16.1.0
25 | container_name: 'node_rest_keycloak'
26 | networks:
27 | - internal
28 | ports:
29 | - '8080:8080'
30 | environment:
31 | KEYCLOAK_USER: admin
32 | KEYCLOAK_PASSWORD: adminPassword@1
33 |
34 | demo-mail-server:
35 | image: mailhog/mailhog
36 | container_name: 'node_rest_mail'
37 | networks:
38 | - internal
39 | logging:
40 | driver: 'none'
41 | ports:
42 | - 1025:1025
43 | - 8025:8025
44 |
45 | multitenancy-rest-service:
46 | build:
47 | context: ./multitenancy-rest-service
48 | dockerfile: Dockerfile
49 | container_name: 'multitenancy-rest-service'
50 | volumes:
51 | - ./multitenancy-rest-service:/usr/src/app
52 | networks:
53 | - internal
54 | depends_on:
55 | - database
56 | - tenant-config-service
57 | - tenant-registration
58 | - keycloak
59 | ports:
60 | - '5000:5000'
61 | env_file:
62 | - ./multitenancy-rest-service/config/.env
63 |
64 | tenant-registration:
65 | build: ./tenant-registration
66 | container_name: 'tenant-registration'
67 | volumes:
68 | - ./tenant-registration:/usr/src/app
69 | networks:
70 | - internal
71 | depends_on:
72 | - database
73 | - tenant-master-service
74 | ports:
75 | - '8875:8875'
76 | env_file:
77 | - ./config/.env
78 | - ./tenant-registration/config/.env
79 |
80 | tenant-master-service:
81 | build: ./tenant-master-service
82 | container_name: 'tenant-master-service'
83 | volumes:
84 | - ./tenant-master-service:/usr/src/app
85 | networks:
86 | - internal
87 | depends_on:
88 | - tenant-provisioning
89 | - tenant-config-service
90 | ports:
91 | - '8847:8847'
92 | env_file:
93 | - ./tenant-master-service/config/.env
94 |
95 | tenant-provisioning:
96 | build: ./tenant-provisioning
97 | container_name: 'tenant-provisioning'
98 | volumes:
99 | - ./tenant-provisioning:/usr/src/app
100 | networks:
101 | - internal
102 | depends_on:
103 | - database
104 | ports:
105 | - '8878:8878'
106 | env_file:
107 | - ./config/.env
108 | - ./tenant-provisioning/config/.env
109 |
110 | tenant-config-service:
111 | build:
112 | context: ./tenant-config-service
113 | dockerfile: Dockerfile
114 | container_name: 'tenant-config-service'
115 | volumes:
116 | - ./tenant-config-service:/usr/src/app
117 | networks:
118 | - internal
119 | depends_on:
120 | - database
121 | ports:
122 | - '8848:8848'
123 | env_file:
124 | - ./config/.env
125 | - ./tenant-config-service/config/.env
126 |
127 | networks:
128 | internal:
129 | volumes:
130 | mysql:
--------------------------------------------------------------------------------
/docker/docker-compose.prod.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | multitenancy-rest-service:
4 | command: npm run start:prod
5 | env_file:
6 | - ./multitenancy-rest-service/config/prod.env
7 |
8 | tenant-registration:
9 | command: npm run start:prod
10 | env_file:
11 | - ./tenant-registration/config/prod.env
12 |
13 | tenant-master-service:
14 | command: npm run start:prod
15 | env_file:
16 | - ./tenant-master-service/config/prod.env
17 |
18 | tenant-provisioning:
19 | command: npm run start:prod
20 | env_file:
21 | - ./tenant-provisioning/config/prod.env
22 |
23 | tenant-config-service:
24 | command: npm run start:prod
25 | env_file:
26 | - ./tenant-config-service/config/prod.env
--------------------------------------------------------------------------------
/docker/docker-compose.staging.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | multitenancy-rest-service:
4 | command: npm run start:staging
5 | env_file:
6 | - ./multitenancy-rest-service/config/staging.env
7 |
8 | tenant-registration:
9 | command: npm run start:staging
10 | env_file:
11 | - ./tenant-registration/config/staging.env
12 |
13 | tenant-master-service:
14 | command: npm run start:staging
15 | env_file:
16 | - ./tenant-master-service/config/staging.env
17 |
18 | tenant-provisioning:
19 | command: npm run start:staging
20 | env_file:
21 | - ./tenant-provisioning/config/staging.env
22 |
23 | tenant-config-service:
24 | command: npm run start:staging
25 | env_file:
26 | - ./tenant-config-service/config/staging.env
27 |
28 |
--------------------------------------------------------------------------------
/init.sql:
--------------------------------------------------------------------------------
1 | CREATE USER nest;
2 |
3 | CREATE DATABASE IF NOT EXISTS rest_api;
4 | GRANT ALL PRIVILEGES ON rest_api.* TO 'user'@'%';
5 | GRANT ALL PRIVILEGES ON rest_api.* TO 'nest'@'%';
6 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/.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/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | root: true,
13 | env: {
14 | node: true,
15 | jest: true,
16 | },
17 | ignorePatterns: ['.eslintrc.js'],
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/explicit-module-boundary-types': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | pnpm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 | package-lock.json
14 |
15 | # OS
16 | .DS_Store
17 |
18 | # Tests
19 | /coverage
20 | /.nyc_output
21 |
22 | # IDEs and editors
23 | /.idea
24 | .project
25 | .classpath
26 | .c9/
27 | *.launch
28 | .settings/
29 | *.sublime-workspace
30 |
31 | # IDE - VSCode
32 | .vscode/*
33 | !.vscode/settings.json
34 | !.vscode/tasks.json
35 | !.vscode/launch.json
36 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/multitenancy-rest-service/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/multitenancy-rest-service/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package*.json ./
6 |
7 | RUN npm install
8 |
9 | COPY . .
10 |
11 | RUN npm run build
12 |
13 | EXPOSE 5000
14 |
15 | CMD [ "npm", "run", "start" ]
--------------------------------------------------------------------------------
/multitenancy-rest-service/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6 | [circleci-url]: https://circleci.com/gh/nestjs/nest
7 |
8 | A progressive Node.js framework for building efficient and scalable server-side applications.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 | ## Description
20 |
21 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
22 |
23 | ## Installation
24 |
25 | ```bash
26 | $ npm install
27 | ```
28 |
29 | ## Running the app
30 |
31 | ```bash
32 | # development
33 | $ npm run start
34 |
35 | # watch mode
36 | $ npm run start:dev
37 |
38 | # production mode
39 | $ npm run start:prod
40 | ```
41 |
42 | ## Test
43 |
44 | ```bash
45 | # unit tests
46 | $ npm run test
47 |
48 | # e2e tests
49 | $ npm run test:e2e
50 |
51 | # test coverage
52 | $ npm run test:cov
53 | ```
54 |
55 | ## Support
56 |
57 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
58 |
59 | ## Stay in touch
60 |
61 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
62 | - Website - [https://nestjs.com](https://nestjs.com/)
63 | - Twitter - [@nestframework](https://twitter.com/nestframework)
64 |
65 | ## License
66 |
67 | Nest is [MIT licensed](LICENSE).
68 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/config/.env:
--------------------------------------------------------------------------------
1 | APP_NAME=multitenancy-rest-service
2 | API_VERSION=v1
3 | APP_ENV=dev
4 | TENANT_CONFIG_HOST=tenant-config-service
5 | TENANT_REGISTRATION_HOST=tenant-registration
6 | TENANT_PROVISIONING_HOST=tenant-provisioning
7 |
8 | # KEYCLOAK_SERVER=https://iam-keycloak.neosoftcloud.org/auth
9 | KEYCLOAK_SERVER=http://keycloak:8080/auth
10 |
11 | CLIENT_ID=my-nest-application
12 | ROOT_URL=http://localhost:5000
13 |
14 | MAIL_SERVER_HOST=demo-mail-server
15 | MAIL_SERVER_PORT=1025
--------------------------------------------------------------------------------
/multitenancy-rest-service/config/prod.env:
--------------------------------------------------------------------------------
1 | APP_NAME=multitenancy-rest-service
2 | API_VERSION=v1
3 | APP_ENV=prod
4 | TENANT_CONFIG_HOST=tenant-config-service
5 | TENANT_REGISTRATION_HOST=tenant-registration
6 | TENANT_PROVISIONING_HOST=tenant-provisioning
7 |
8 | KEYCLOAK_SERVER=https://iam-keycloak.neosofttech.com/auth
9 |
10 | CLIENT_ID=my-nest-application
11 | ROOT_URL=http://host:5000
12 |
13 | KEYCLOAK_REDIRECT_URL=http://host:8080/auth
14 |
15 | MAIL_SERVER_HOST=demo-mail-server
16 | MAIL_SERVER_PORT=1025
17 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/config/staging.env:
--------------------------------------------------------------------------------
1 | APP_NAME=multitenancy-rest-service
2 | API_VERSION=v1
3 | APP_ENV=staging
4 | TENANT_CONFIG_HOST=tenant-config-service
5 | TENANT_REGISTRATION_HOST=tenant-registration
6 | TENANT_PROVISIONING_HOST=tenant-provisioning
7 |
8 | KEYCLOAK_SERVER=https://iam-keycloak.neosofttech.com/auth
9 |
10 | CLIENT_ID=my-nest-application
11 | ROOT_URL=http://host:5000
12 |
13 | KEYCLOAK_REDIRECT_URL=http://host:8080/auth
14 |
15 | MAIL_SERVER_HOST=demo-mail-server
16 | MAIL_SERVER_PORT=1025
17 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src",
4 | "compilerOptions": {
5 | "plugins": ["@nestjs/swagger"]
6 | }
7 | }
--------------------------------------------------------------------------------
/multitenancy-rest-service/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "multitenancy-rest-service",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start",
13 | "start:dev": "nest start --watch",
14 | "start:staging": "nest start",
15 | "start:debug": "nest start --debug --watch",
16 | "start:prod": "node dist/main",
17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
18 | "test": "jest",
19 | "test:watch": "jest --watch",
20 | "test:cov": "jest --coverage",
21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
22 | "test:e2e": "jest .e2e-spec.ts$"
23 | },
24 | "dependencies": {
25 | "@keycloak/keycloak-admin-client": "^16.1.0",
26 | "@nestjs/common": "^8.0.0",
27 | "@nestjs/config": "^1.1.5",
28 | "@nestjs/core": "^8.0.0",
29 | "@nestjs/microservices": "^8.2.4",
30 | "@nestjs/platform-express": "^8.0.0",
31 | "axios": "^0.25.0",
32 | "class-transformer": "^0.5.1",
33 | "class-validator": "^0.13.2",
34 | "jsonwebtoken": "^8.5.1",
35 | "jwks-rsa": "^2.0.5",
36 | "mysql2": "^2.3.3",
37 | "querystring": "^0.2.1",
38 | "reflect-metadata": "^0.1.13",
39 | "rimraf": "^3.0.2",
40 | "rxjs": "^7.2.0",
41 | "winston": "^3.7.2"
42 | },
43 | "devDependencies": {
44 | "@nestjs/cli": "^8.0.0",
45 | "@nestjs/schematics": "^8.0.0",
46 | "@nestjs/swagger": "^5.1.5",
47 | "@nestjs/testing": "^8.2.5",
48 | "@types/express": "^4.17.13",
49 | "@types/jest": "^26.0.24",
50 | "@types/jsonwebtoken": "^8.5.8",
51 | "@types/node": "^16.0.0",
52 | "@types/supertest": "^2.0.11",
53 | "@typescript-eslint/eslint-plugin": "^4.28.2",
54 | "@typescript-eslint/parser": "^4.28.2",
55 | "eslint": "^7.30.0",
56 | "eslint-config-prettier": "^8.3.0",
57 | "eslint-plugin-prettier": "^3.4.0",
58 | "jest": "27.0.6",
59 | "node-mocks-http": "^1.11.0",
60 | "prettier": "^2.3.2",
61 | "supertest": "^6.1.3",
62 | "swagger-ui-express": "^4.3.0",
63 | "ts-jest": "^27.0.3",
64 | "ts-loader": "^9.2.3",
65 | "ts-node": "^10.0.0",
66 | "tsconfig-paths": "^3.12.0",
67 | "typescript": "^4.3.5"
68 | },
69 | "jest": {
70 | "moduleFileExtensions": [
71 | "js",
72 | "json",
73 | "ts"
74 | ],
75 | "rootDir": ".",
76 | "testRegex": ".*\\.spec\\.ts$",
77 | "transform": {
78 | "^.+\\.(t|j)s$": "ts-jest"
79 | },
80 | "collectCoverageFrom": [
81 | "**/*.(t|j)s"
82 | ],
83 | "coverageDirectory": "../coverage",
84 | "testEnvironment": "node",
85 | "moduleNameMapper": {
86 | "@app/(.*)": "/src/$1"
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule } from '@nestjs/config';
3 | import { ClientsModule, Transport } from '@nestjs/microservices';
4 | import { AppController } from './app.controller';
5 | import { AppService } from './app.service';
6 | import { AuthService } from './auth/auth.service';
7 | import { PublicKeyCache } from './auth/cache.publicKey';
8 | import { KeycloakAuthGuard } from './auth/guards/keycloak-auth.guard';
9 | import config from './config';
10 | import { Keycloak, KeycloakAuthPolicy, KeycloakAuthResource, KeycloakClient, KeycloakRealm, KeycloakUser, KeycloakAuthScope, KeycloakAuthPermission } from './iam';
11 |
12 | @Module({
13 | imports: [
14 | ConfigModule.forRoot({
15 | envFilePath: [`${process.cwd()}/config/.env`],
16 | isGlobal: true,
17 | expandVariables: true,
18 | load: config,
19 | }),
20 | ClientsModule.register([
21 | {
22 | name: 'REGISTER_TENANT',
23 | transport: Transport.TCP,
24 | options: {
25 | host: process.env.TENANT_REGISTRATION_HOST,
26 | port: 8875,
27 | },
28 | },
29 | {
30 | name: 'GET_TENANT_CONFIG',
31 | transport: Transport.TCP,
32 | options: {
33 | host: process.env.TENANT_CONFIG_HOST,
34 | port: 8848,
35 | },
36 | },
37 | {
38 | name: 'CREATE_TABLE',
39 | transport: Transport.TCP,
40 | options: {
41 | host: process.env.TENANT_PROVISIONING_HOST,
42 | port: 8878,
43 | },
44 | },
45 | ]),
46 | ],
47 | controllers: [AppController],
48 | providers: [
49 | AppService, AuthService, PublicKeyCache, KeycloakAuthGuard,
50 | Keycloak, KeycloakUser, KeycloakRealm, KeycloakAuthPolicy,
51 | KeycloakAuthResource, KeycloakClient, KeycloakAuthScope, KeycloakAuthPermission
52 | ],
53 | })
54 | export class AppModule { }
55 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/auth/cache.publicKey.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@nestjs/common";
2 | import * as jwt from "jsonwebtoken";
3 | import * as jwksClient from "jwks-rsa";
4 |
5 | @Injectable()
6 | export class PublicKeyCache {
7 | private readonly millisecondsToLive: number;
8 | private cache: Record = {};
9 | private kid: string;
10 | private readonly minutesToLive = 10;
11 |
12 | constructor() {
13 | this.millisecondsToLive = this.minutesToLive * 60 * 1000;
14 | }
15 |
16 | isCacheExpired() {
17 | return (this.cache[this.kid].date.getTime() + this.millisecondsToLive) < new Date().getTime();
18 | }
19 | async getPublicKey(token: string) {
20 | this.kid = this.getkid(token)
21 | if (!this.cache[this.kid] || this.isCacheExpired()) {
22 | const data = await this.fetchPublicKey(token);
23 | this.cache[this.kid] = {
24 | key: data,
25 | date: new Date()
26 | };
27 | return data;
28 | } else {
29 | return Promise.resolve(this.cache[this.kid].key);
30 | }
31 | }
32 |
33 | private async fetchPublicKey(token: string) {
34 | const { iss }: any = jwt.decode(token) as jwt.JwtPayload;
35 | const client = jwksClient({
36 | jwksUri: `${iss}/protocol/openid-connect/certs`
37 | });
38 | const key = await client.getSigningKey(this.kid);
39 | return key.getPublicKey();
40 | }
41 |
42 | private getkid(token: string) {
43 | const { header } = jwt.decode(token, { complete: true });
44 | return header.kid;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/auth/guards/keycloak-auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common';
2 | import { Reflector } from '@nestjs/core';
3 | import { Request } from 'express';
4 | import { AuthService } from '../auth.service';
5 | import { PublicKeyCache } from '../cache.publicKey';
6 |
7 | @Injectable()
8 | export class KeycloakAuthGuard implements CanActivate {
9 | constructor(
10 | private readonly authenticationService: AuthService,
11 | private readonly publicKeyCache: PublicKeyCache,
12 | private readonly reflector: Reflector,
13 | ) { }
14 |
15 | async canActivate(
16 | context: ExecutionContext,
17 | ): Promise {
18 |
19 | const request: Request = context.switchToHttp().getRequest();
20 | const roles = this.reflector.get('roles', context.getHandler());
21 | const permissions = this.reflector.get('permissions', context.getHandler());
22 | const header = request.header('Authorization');
23 | if (!header) {
24 | throw new HttpException(
25 | 'Authorization: Bearer header missing',
26 | HttpStatus.UNAUTHORIZED,
27 | );
28 | }
29 | const parts = header.split(' ');
30 | if (parts.length !== 2 || parts[0] !== 'Bearer') {
31 | throw new HttpException(
32 | 'Authorization: Bearer header invalid',
33 | HttpStatus.UNAUTHORIZED,
34 | );
35 | }
36 |
37 | const token = parts[1];
38 | try {
39 | const publicKey = await this.publicKeyCache.getPublicKey(token);
40 | await this.authenticationService.validateTokenwithKey(token, publicKey);
41 |
42 | let usrRole = true, usrPermission = true;
43 | if (roles) {
44 | const userRoles: string[] = await this.authenticationService.getRoles(token);
45 | if (!userRoles) {
46 | return false;
47 | }
48 | usrRole = await this.hasRole(userRoles, roles);
49 | }
50 | if (permissions) {
51 | const userPermissions: string[] = await this.authenticationService.getPermissions(token);
52 | if (!userPermissions) {
53 | return false;
54 | }
55 | usrPermission = await this.hasPermission(userPermissions, permissions);
56 | }
57 | return usrRole && usrPermission;
58 | } catch (e) {
59 | throw new HttpException(e.message, HttpStatus.UNAUTHORIZED);
60 | }
61 | }
62 |
63 | async hasRole(userRoles: string[], roles: string[]): Promise {
64 | const containsAny: boolean = roles.some(role => {
65 | return userRoles.includes(role);
66 | })
67 | return containsAny;
68 | }
69 |
70 | async hasPermission(userPermissions: string[], permissions: string[]): Promise {
71 | const containsAny: boolean = permissions.some(permission => {
72 | return userPermissions.includes(permission);
73 | })
74 | return containsAny;
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/auth/permission.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 |
3 | export const Permissions = (permissions: string[]) => SetMetadata('permissions', permissions);
4 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/auth/roles.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 |
3 | export const Roles = (roles: string[]) => SetMetadata('roles', roles);
4 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/config/app.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('app', () => ({
4 | name: process.env.APP_NAME || 'multitenancy-rest-service',
5 | version: process.env.API_VERSION || 'v1',
6 | env: process.env.APP_ENV || 'local',
7 | port: +process.env.APP_PORT || 5000,
8 | }));
9 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/config/client.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('client', () => ({
4 | id: process.env.CLIENT_ID,
5 | rootUrl: process.env.ROOT_URL
6 | }));
7 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/config/index.ts:
--------------------------------------------------------------------------------
1 | import app from './app';
2 | import client from './client';
3 | import keycloak from './keycloak';
4 | import mailServer from './mail.server';
5 |
6 | export default [app, keycloak, client, mailServer];
7 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/config/keycloak.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('keycloak', () => ({
4 | server: process.env.KEYCLOAK_SERVER || 'https://keycloak:8080/auth',
5 | key: process.env.KEY
6 | }));
7 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/config/mail.server.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('mailServer', () => ({
4 | host: process.env.MAIL_SERVER_HOST || "demo-mail-server",
5 | port: +process.env.MAIL_SERVER_PORT || 1025
6 | }));
7 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/client.dto.ts:
--------------------------------------------------------------------------------
1 | import ClientRepresentation from '@keycloak/keycloak-admin-client/lib/defs/clientRepresentation';
2 | import protocolMapperRepresentation from '@keycloak/keycloak-admin-client/lib/defs/protocolMapperRepresentation';
3 | import { ApiProperty } from '@nestjs/swagger';
4 | import { IsNotEmpty, IsString } from 'class-validator';
5 |
6 | class ClientDetails implements ClientRepresentation {
7 | clientId?: string;
8 | rootUrl?: string;
9 | redirectUris?: string[];
10 | serviceAccountsEnabled?: boolean;
11 | authorizationServicesEnabled?: boolean;
12 | directAccessGrantsEnabled?: boolean;
13 | protocolMappers?: protocolMapperRepresentation[];
14 | }
15 |
16 | export class ClientDto {
17 | @ApiProperty()
18 | @IsNotEmpty()
19 | @IsString()
20 | tenantName: string;
21 |
22 | clientDetails?: ClientDetails;
23 | }
24 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/column.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from "@nestjs/swagger";
2 |
3 | export class ColumnDto {
4 | @ApiProperty()
5 | columnName: string;
6 | @ApiProperty()
7 | columntype: any;
8 | }
9 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/create.realm.dto.ts:
--------------------------------------------------------------------------------
1 | import ClientRepresentation from '@keycloak/keycloak-admin-client/lib/defs/clientRepresentation';
2 | import { ApiProperty } from '@nestjs/swagger';
3 | import { IsEmail, IsNotEmpty, IsString, Matches } from 'class-validator';
4 |
5 | class ClientDetails implements ClientRepresentation {
6 | clientId?: string;
7 | rootUrl?: string;
8 | redirectUris?: string[];
9 | serviceAccountsEnabled?: boolean;
10 | authorizationServicesEnabled?: boolean;
11 | directAccessGrantsEnabled?: boolean;
12 | }
13 |
14 | export class CreateRealmDto {
15 | @ApiProperty()
16 | @IsNotEmpty()
17 | @IsString()
18 | tenantName: string;
19 |
20 | @ApiProperty()
21 | @IsNotEmpty()
22 | @IsString()
23 | userName: string;
24 |
25 | @ApiProperty()
26 | @IsNotEmpty()
27 | @IsEmail()
28 | email: string;
29 |
30 | @ApiProperty()
31 | @IsNotEmpty()
32 | @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, {
33 | message: 'password must be minimum eight characters, at least one uppercase letter, one lowercase letter, one number and ' +
34 | 'one special character'
35 | })
36 | password: string;
37 | }
38 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/create.role.dto.ts:
--------------------------------------------------------------------------------
1 | import RoleRepresentation, { Composites } from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
2 |
3 | class RoleDetails implements RoleRepresentation {
4 | name?: string;
5 | description?: string;
6 | composite?: boolean;
7 | composites?: Composites;
8 | }
9 |
10 | export class CreateRoleDto {
11 | tenantName: string;
12 | roleDetails: RoleDetails;
13 | }
14 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/credentials.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiHideProperty } from '@nestjs/swagger';
2 | import { IsNotEmpty, Matches } from 'class-validator';
3 |
4 | export class CredentialsDto {
5 | tenantName: string;
6 | username: string;
7 |
8 | @IsNotEmpty()
9 | @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, {
10 | message: 'password must be minimum eight characters, at least one uppercase letter, one lowercase letter, one number and ' +
11 | 'one special character'
12 | })
13 | password: string;
14 |
15 | @ApiHideProperty()
16 | clientId: string;
17 |
18 | @ApiHideProperty()
19 | clientSecret: string
20 | }
21 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/db.details.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 |
3 | export class DbDetailsDto {
4 | @ApiProperty()
5 | host: string;
6 | @ApiProperty()
7 | port: number;
8 | @ApiProperty()
9 | tenantName: string;
10 | @ApiProperty()
11 | password: string;
12 | @ApiProperty()
13 | dbName: string;
14 | }
15 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/delete.permission.dto.ts:
--------------------------------------------------------------------------------
1 | export class DeletePermissionDto {
2 | tenantName: string;
3 | clientName: string;
4 | permissionName: string;
5 | permissionType: string;
6 | }
7 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/delete.role.dto.ts:
--------------------------------------------------------------------------------
1 | export class DeleteRoleDto {
2 | tenantName: string;
3 | roleName: string;
4 | }
5 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/getpermissions.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiHideProperty } from "@nestjs/swagger"
2 |
3 | export class GetPermissionsDto {
4 | tenantName?: string
5 |
6 | @ApiHideProperty()
7 | clientName: string
8 | }
9 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/index.ts:
--------------------------------------------------------------------------------
1 | import { ColumnDto } from "./column.dto";
2 | import { CredentialsDto } from "./credentials.dto";
3 | import { LogoutDto } from "./logout.body.dto";
4 | import { DbDetailsDto } from "./db.details.dto";
5 | import { ProvisionTenantTableDto } from "./provision.tenant.table.dto";
6 | import { RegisterTenantDto } from "./register.tenant.dto";
7 | import { TenantUserDto } from "./tenant.user.dto";
8 | import { UpdateTenantDto } from "./update.tenant.dto ";
9 | import { ResourceDto } from "./resource.dto";
10 | import { PolicyDto } from "./policy.dto";
11 | import { ClientDto } from "./client.dto";
12 | import { ScopeDto } from "./scope.dto";
13 | import { ScopeRepresentationDto } from "./scope.representation.dto";
14 | import { UserDetailsDto } from "./user.details.dto";
15 | import { UsersQueryDto } from "./users.query.dto";
16 | import { DeleteUserDto } from "./user.delete.dto";
17 | import { UpdateUserDto } from "./user.update.dto";
18 | import { PermissionDto } from "./permission.dto";
19 | import { CreateRealmDto } from "./create.realm.dto";
20 | import { RefreshAccessTokenDto } from "./refresh.access-token.dto";
21 | import { GetUsersInfoDto } from "./user.getinfo.dto";
22 | import { CreateRoleDto } from "./create.role.dto";
23 | import { UpdateRoleDto } from "./update.role.dto";
24 | import { DeleteRoleDto } from "./delete.role.dto";
25 | import { GetRoleInfoDto } from "./role.getinfo.dto";
26 | import { GetPermissionsDto } from "./getpermissions.dto";
27 | import { UpdatePermissionDto } from "./update.permission.dto";
28 | import { DeletePermissionDto } from "./delete.permission.dto";
29 |
30 |
31 | export {
32 | ColumnDto,
33 | CredentialsDto,
34 | LogoutDto,
35 | DbDetailsDto,
36 | ProvisionTenantTableDto,
37 | RegisterTenantDto,
38 | TenantUserDto,
39 | UpdateTenantDto,
40 | ResourceDto,
41 | PolicyDto,
42 | ClientDto,
43 | ScopeDto,
44 | ScopeRepresentationDto,
45 | UserDetailsDto,
46 | UsersQueryDto,
47 | DeleteUserDto,
48 | UpdateUserDto,
49 | PermissionDto,
50 | CreateRealmDto,
51 | RefreshAccessTokenDto,
52 | GetUsersInfoDto,
53 | CreateRoleDto,
54 | UpdateRoleDto,
55 | DeleteRoleDto,
56 | GetRoleInfoDto,
57 | GetPermissionsDto,
58 | UpdatePermissionDto,
59 | DeletePermissionDto
60 | }
61 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/logout.body.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
2 | import { IsJWT, IsNotEmpty } from 'class-validator';
3 |
4 | export class LogoutDto {
5 | @ApiHideProperty()
6 | tenantName: string;
7 |
8 | @ApiProperty()
9 | @IsNotEmpty()
10 | @IsJWT()
11 | refreshToken: string;
12 |
13 | @ApiHideProperty()
14 | clientId: string;
15 |
16 | @ApiHideProperty()
17 | clientSecret: string
18 | }
19 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/permission.dto.ts:
--------------------------------------------------------------------------------
1 | import PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
2 | import { ApiProperty } from "@nestjs/swagger";
3 | import { IsNotEmpty, IsString } from "class-validator";
4 |
5 | class PermissionDetails implements PolicyRepresentation{
6 | name?: string;
7 | description?: string;
8 | scopes?: string[];
9 | resources?: string[];
10 | }
11 |
12 | export class PermissionDto{
13 | @ApiProperty()
14 | @IsNotEmpty()
15 | @IsString()
16 | tenantName: string;
17 | clientName: string;
18 | permissionType: string;
19 | permissionDetails: PermissionDetails;
20 | }
21 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/policy.dto.ts:
--------------------------------------------------------------------------------
1 | import PolicyRepresentation from '@keycloak/keycloak-admin-client/lib/defs/policyRepresentation';
2 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
3 | import { IsNotEmpty, IsString } from 'class-validator';
4 |
5 | class PolicyDetails implements PolicyRepresentation {
6 | name?: string;
7 | description?: string;
8 | users?: string[];
9 | roles?: Record[];
10 | @ApiHideProperty()
11 | policies?: string[];
12 | @ApiHideProperty()
13 | notBefore?:string;
14 | @ApiHideProperty()
15 | notOnOrAfter?:string
16 | }
17 |
18 | export class PolicyDto {
19 | @ApiProperty()
20 | @IsNotEmpty()
21 | @IsString()
22 | tenantName: string;
23 |
24 | policyType: string
25 | clientName: string
26 | policyDetails: PolicyDetails
27 | }
28 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/provision.tenant.table.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { ColumnDto } from './column.dto';
3 |
4 | export class ProvisionTenantTableDto {
5 | @ApiProperty()
6 | dbName: string;
7 | @ApiProperty()
8 | tableName: string;
9 | @ApiProperty()
10 | columns: ColumnDto[];
11 | }
12 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/realm.dto.ts:
--------------------------------------------------------------------------------
1 | export interface Realm {
2 | realmName: string
3 | }
4 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/refresh.access-token.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
2 | import { IsJWT, IsNotEmpty } from 'class-validator';
3 |
4 | export class RefreshAccessTokenDto {
5 | @ApiHideProperty()
6 | tenantName: string;
7 |
8 | @ApiProperty()
9 | @IsNotEmpty()
10 | @IsJWT()
11 | refreshToken: string;
12 |
13 | @ApiHideProperty()
14 | clientId: string;
15 |
16 | @ApiHideProperty()
17 | clientSecret: string
18 | }
19 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/register.tenant.dto.ts:
--------------------------------------------------------------------------------
1 | import ClientRepresentation from '@keycloak/keycloak-admin-client/lib/defs/clientRepresentation';
2 | import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
3 | import { IsEmail, IsNotEmpty, IsString, Matches } from 'class-validator';
4 |
5 | class ClientDetails implements ClientRepresentation {
6 | clientId?: string;
7 | rootUrl?: string;
8 | redirectUris?: string[];
9 | serviceAccountsEnabled?: boolean;
10 | authorizationServicesEnabled?: boolean;
11 | directAccessGrantsEnabled?: boolean;
12 | }
13 |
14 | export class RegisterTenantDto {
15 | @ApiProperty()
16 | @IsNotEmpty()
17 | @IsString()
18 | tenantName: string;
19 |
20 | @ApiProperty()
21 | @IsNotEmpty()
22 | @IsString()
23 | userName: string;
24 |
25 | @ApiProperty()
26 | @IsNotEmpty()
27 | @IsEmail()
28 | email: string;
29 |
30 | @ApiProperty()
31 | @IsNotEmpty()
32 | @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, {
33 | message: 'password must be minimum eight characters, at least one uppercase letter, one lowercase letter, one number and ' +
34 | 'one special character'
35 | })
36 | password: string;
37 |
38 | @ApiProperty()
39 | @IsNotEmpty()
40 | description: string;
41 |
42 | @ApiProperty()
43 | @IsNotEmpty()
44 | databaseName: string;
45 |
46 | @ApiProperty()
47 | @IsNotEmpty()
48 | databaseDescription: string;
49 |
50 | @ApiHideProperty()
51 | createdDateTime?: string;
52 |
53 | @ApiHideProperty()
54 | clientDetails?: ClientDetails
55 |
56 | @ApiHideProperty()
57 | clientId?: string
58 |
59 | @ApiHideProperty()
60 | clientSecret?: string
61 | }
62 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/resource.dto.ts:
--------------------------------------------------------------------------------
1 | import ResourceRepresentation from '@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation';
2 | import { ApiProperty } from '@nestjs/swagger';
3 | import { IsNotEmpty, IsString } from 'class-validator';
4 |
5 | class ResourceDetails implements ResourceRepresentation {
6 | name?: string;
7 | uris?: string[];
8 | }
9 |
10 | export class ResourceDto {
11 | @ApiProperty()
12 | @IsNotEmpty()
13 | @IsString()
14 | tenantName: string;
15 |
16 | clientName: string;
17 | resourceDetails: ResourceDetails
18 | }
19 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/role.getinfo.dto.ts:
--------------------------------------------------------------------------------
1 | export class GetRoleInfoDto {
2 | tenantName: string
3 | roleName: string
4 | }
5 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/scope.dto.ts:
--------------------------------------------------------------------------------
1 | import { ScopeRepresentationDto } from "./scope.representation.dto";
2 |
3 | export class ScopeDto {
4 | tenantName: string;
5 | clientName: string;
6 | scopeDetails: ScopeRepresentationDto;
7 | }
8 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/scope.representation.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiHideProperty } from "@nestjs/swagger";
2 |
3 | export class ScopeRepresentationDto {
4 | name: string;
5 |
6 | @ApiHideProperty()
7 | displayName?: string;
8 |
9 | @ApiHideProperty()
10 | iconUri?: string;
11 |
12 | @ApiHideProperty()
13 | id?: string;
14 | }
15 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/tenant.adminuser.dto.ts:
--------------------------------------------------------------------------------
1 | export interface TenantAdminUser {
2 | id: string
3 | }
4 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/tenant.user.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiHideProperty } from '@nestjs/swagger';
2 | import { Type } from 'class-transformer';
3 | import { ValidateNested } from 'class-validator';
4 | import { UserDetailsDto } from './user.details.dto';
5 |
6 | export class TenantUserDto {
7 | @ApiHideProperty()
8 | tenantName: string;
9 |
10 | @ValidateNested({ each: true })
11 | @Type(() => UserDetailsDto)
12 | userDetails: UserDetailsDto;
13 | }
14 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/update.permission.dto.ts:
--------------------------------------------------------------------------------
1 | import PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
2 |
3 | class PermissionDetails implements PolicyRepresentation {
4 | name?: string;
5 | description?: string;
6 | scopes?: string[];
7 | resources?: string[];
8 | }
9 |
10 | export class UpdatePermissionDto {
11 | tenantName: string;
12 | clientName: string;
13 | permissionName: string;
14 | permissionType: string;
15 | permissionDetails: PermissionDetails;
16 | }
17 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/update.role.dto.ts:
--------------------------------------------------------------------------------
1 | import RoleRepresentation, { Composites } from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
2 |
3 | class RoleDetails implements RoleRepresentation {
4 | name?: string;
5 | description?: string;
6 | composite?: boolean;
7 | composites?: Composites;
8 | }
9 |
10 | export class UpdateRoleDto {
11 | tenantName: string;
12 | roleName: string;
13 | action: RoleDetails;
14 | }
15 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/update.tenant.dto .ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 |
3 | class TenantDetails {
4 | @ApiProperty()
5 | tenantName: string;
6 | @ApiProperty()
7 | description: string;
8 | }
9 |
10 | export class UpdateTenantDto {
11 | @ApiProperty()
12 | action: TenantDetails;
13 | }
14 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/user.delete.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiHideProperty } from "@nestjs/swagger";
2 |
3 | export class DeleteUserDto {
4 | @ApiHideProperty()
5 | tenantName: string;
6 |
7 | userName: string;
8 | }
9 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/user.details.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsEmail, IsNotEmpty, IsString, Matches } from 'class-validator';
3 |
4 | export class UserDetailsDto {
5 | @ApiProperty()
6 | @IsNotEmpty()
7 | @IsString()
8 | userName: string;
9 |
10 | @ApiProperty()
11 | @IsNotEmpty()
12 | @IsEmail()
13 | email: string;
14 |
15 | @ApiProperty()
16 | @IsNotEmpty()
17 | @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, {
18 | message: 'password must be minimum eight characters, at least one uppercase letter, one lowercase letter, one number and ' +
19 | 'one special character'
20 | })
21 | password: string;
22 |
23 | roles: string[];
24 |
25 | attributes: string[];
26 | }
27 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/user.getinfo.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiHideProperty } from "@nestjs/swagger"
2 |
3 | export class GetUsersInfoDto {
4 | tenantName?: string
5 | userName?: string
6 |
7 | @ApiHideProperty()
8 | clientName?: string
9 | }
10 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/user.update.dto.ts:
--------------------------------------------------------------------------------
1 | import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
2 | import { ApiHideProperty } from "@nestjs/swagger";
3 |
4 | class UserDetails implements UserRepresentation {
5 | firstName?: string;
6 | lastName?: string;
7 | email?: string;
8 | enabled?: boolean;
9 | realmRoles?: string[];
10 | }
11 |
12 | export class UpdateUserDto {
13 | @ApiHideProperty()
14 | tenantName: string;
15 |
16 | userName: string;
17 | action: UserDetails;
18 | }
19 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/dto/users.query.dto.ts:
--------------------------------------------------------------------------------
1 | export class UsersQueryDto {
2 | tenantName?: string
3 | userName?: string
4 | page?: number
5 | }
6 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/filter/http-exception.filter.ts:
--------------------------------------------------------------------------------
1 | import { ExceptionFilter, Catch, ArgumentsHost, HttpException, Logger, HttpStatus } from '@nestjs/common';
2 | import { Response } from 'express';
3 |
4 | @Catch()
5 | export class HttpErrorFilter implements ExceptionFilter {
6 | private readonly logger: Logger
7 | constructor() {
8 | this.logger = new Logger('EXCEPTION')
9 | }
10 |
11 | catch(exception: any, host: ArgumentsHost) {
12 | const ctx = host.switchToHttp();
13 | const response = ctx.getResponse();
14 |
15 | let statusCode: number, message: any;
16 | if (exception.response && exception.response.statusCode) {
17 | statusCode = exception.response.statusCode
18 | message = exception.response.message
19 | }
20 | else if (exception.response && exception.response.status) {
21 | statusCode = exception.response.status
22 | message = exception.response.data
23 | }
24 | else if (exception.status) {
25 | statusCode = exception.status
26 | message = exception.message
27 | }
28 | else if (exception instanceof HttpException) {
29 | statusCode = exception.getStatus()
30 | message = exception.message
31 | }
32 | else {
33 | statusCode = HttpStatus.INTERNAL_SERVER_ERROR
34 | message = 'Internal server error'
35 | }
36 |
37 | this.logger.error({ statusCode, message });
38 |
39 | response
40 | .status(statusCode)
41 | .json({
42 | statusCode,
43 | message
44 | });
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/iam/client.ts:
--------------------------------------------------------------------------------
1 | import KcAdminClient from '@keycloak/keycloak-admin-client';
2 | import { Injectable, NotFoundException } from '@nestjs/common';
3 | import { ConfigService } from '@nestjs/config';
4 | import ClientRepresentation from '@keycloak/keycloak-admin-client/lib/defs/clientRepresentation';
5 | import { ClientDto } from '../dto';
6 |
7 | @Injectable()
8 | export class KeycloakClient {
9 | private kcTenantAdminClient: KcAdminClient;
10 | constructor(
11 | private readonly config: ConfigService
12 | ) { }
13 |
14 | public async createClient(body: ClientDto, token: string): Promise<{
15 | clientId: string;
16 | clientSecret: string;
17 | }> {
18 | const { tenantName, clientDetails } = body;
19 | this.kcTenantAdminClient = new KcAdminClient({
20 | baseUrl: this.config.get('keycloak.server'),
21 | realmName: tenantName
22 | });
23 |
24 | const parts = token.split(' ')
25 | this.kcTenantAdminClient.setAccessToken(parts[1]);
26 |
27 | await this.kcTenantAdminClient.clients.create(clientDetails);
28 | const clientSecret = await this.generateSecret(this.kcTenantAdminClient, clientDetails.clientId);
29 |
30 | return {
31 | clientId: clientDetails.clientId,
32 | clientSecret
33 | };
34 | }
35 |
36 | public async findClient(kcclient: KcAdminClient, clientName: string): Promise {
37 | const clients = await kcclient.clients.find();
38 | const client = clients.filter((Client) => Client.clientId === clientName);
39 | if (!client[0]) {
40 | throw new NotFoundException('Client not found');
41 | }
42 | return client[0];
43 | }
44 |
45 | private async generateSecret(kcclient: KcAdminClient, clientName: string): Promise {
46 | const clients = await kcclient.clients.find();
47 | const client = clients.filter((Client) => Client.clientId === clientName);
48 | const newCredential = await kcclient.clients.generateNewClientSecret(
49 | {
50 | id: client[0].id,
51 | },
52 | );
53 | return newCredential.value;
54 | }
55 |
56 | public defaultClientDetails() {
57 | return {
58 | clientId: this.config.get('client.id'),
59 | rootUrl: this.config.get('client.rootUrl'),
60 | redirectUris: [`${this.config.get('client.rootUrl')}/*`],
61 | serviceAccountsEnabled: true,
62 | authorizationServicesEnabled: true,
63 | directAccessGrantsEnabled: true,
64 | protocolMappers: [{
65 | name: "permissionMapper",
66 | protocol: "openid-connect",
67 | protocolMapper: "oidc-usermodel-attribute-mapper",
68 | config: {
69 | "multivalued": "true",
70 | "access.token.claim": "true",
71 | "claim.name": "permission",
72 | "user.attribute": "permission"
73 | }
74 | }]
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/iam/index.ts:
--------------------------------------------------------------------------------
1 | import { Keycloak } from "./keycloak"
2 | import { KeycloakRealm } from "./keycloakRealm"
3 | import { KeycloakUser } from "./keycloakUser"
4 | import { KeycloakClient } from "./client"
5 | import { KeycloakAuthPolicy } from "./policy"
6 | import { KeycloakAuthResource } from "./resource"
7 | import { KeycloakAuthScope } from "./scope"
8 | import { KeycloakAuthPermission } from "./permission"
9 |
10 | export {
11 | Keycloak,
12 | KeycloakRealm,
13 | KeycloakUser,
14 | KeycloakClient,
15 | KeycloakAuthPolicy,
16 | KeycloakAuthResource,
17 | KeycloakAuthScope,
18 | KeycloakAuthPermission
19 | }
20 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/iam/keycloak.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@nestjs/common";
2 | import KcAdminClient from '@keycloak/keycloak-admin-client';
3 | import { ConfigService } from '@nestjs/config';
4 |
5 | @Injectable()
6 | export class Keycloak {
7 | constructor(private readonly config: ConfigService) { }
8 |
9 | public kcMasterAdminClient: KcAdminClient = new KcAdminClient({
10 | baseUrl: this.config.get('keycloak.server'),
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/iam/permission.ts:
--------------------------------------------------------------------------------
1 | import KcAdminClient from '@keycloak/keycloak-admin-client';
2 | import { Injectable } from "@nestjs/common";
3 | import { ConfigService } from "@nestjs/config";
4 | import { KeycloakClient } from "./client";
5 | import { DeletePermissionDto, PermissionDto, UpdatePermissionDto } from '../dto';
6 |
7 | @Injectable()
8 | export class KeycloakAuthPermission {
9 | private kcAdminClient: KcAdminClient;
10 | constructor(
11 | private readonly keycloakClient: KeycloakClient,
12 | private readonly config: ConfigService
13 | ) {
14 | this.keycloakServer = this.config.get('keycloak.server');
15 | }
16 | keycloakServer: string;
17 |
18 | public async createPermission(body: PermissionDto, token: string): Promise {
19 | const { tenantName, clientName, permissionType, permissionDetails } = body;
20 | this.kcAdminClient = new KcAdminClient({
21 | baseUrl: this.keycloakServer,
22 | realmName: tenantName
23 | });
24 |
25 | const parts = token.split(' ')
26 | this.kcAdminClient.setAccessToken(parts[1]);
27 |
28 |
29 | await this.kcAdminClient.clients.createPermission(
30 | {
31 | id: (await this.keycloakClient.findClient(this.kcAdminClient, clientName)).id,
32 | type: permissionType
33 | },
34 | permissionDetails
35 | );
36 | return "Permission created successfully";
37 | }
38 |
39 | public async getPermissions(tenantName: string, clientName: string, token: string) {
40 | const kcClient = new KcAdminClient({
41 | baseUrl: this.keycloakServer,
42 | realmName: tenantName
43 | });
44 |
45 | const parts = token.split(' ')
46 | kcClient.setAccessToken(parts[1]);
47 |
48 | return kcClient.clients.findPermissions({
49 | id: (await this.keycloakClient.findClient(kcClient, clientName)).id,
50 | name: ''
51 | });
52 | }
53 |
54 | public async updatePermission(body: UpdatePermissionDto, token: string): Promise {
55 | const { tenantName, clientName, permissionName, permissionType, permissionDetails } = body;
56 | const kcClient = new KcAdminClient({
57 | baseUrl: this.keycloakServer,
58 | realmName: tenantName
59 | });
60 |
61 | const parts = token.split(' ')
62 | kcClient.setAccessToken(parts[1]);
63 |
64 | const client = await kcClient.clients.find({
65 | clientId: clientName
66 | });
67 | const permission = await kcClient.clients.findPermissions({
68 | id: client[0].id,
69 | name: permissionName
70 | });
71 | await kcClient.clients.updatePermission(
72 | {
73 | id: client[0].id,
74 | type: permissionType,
75 | permissionId: permission[0].id
76 | },
77 | {
78 | ...permission[0],
79 | ...permissionDetails
80 | }
81 | );
82 | return "Permission updated successfully";
83 | }
84 |
85 | public async deletePermission(body: DeletePermissionDto, token: string): Promise {
86 | const { tenantName, clientName, permissionName, permissionType } = body;
87 | const kcClient = new KcAdminClient({
88 | baseUrl: this.keycloakServer,
89 | realmName: tenantName
90 | });
91 |
92 | const parts = token.split(' ')
93 | kcClient.setAccessToken(parts[1]);
94 |
95 | const client = await kcClient.clients.find({
96 | clientId: clientName
97 | });
98 | const permission = await kcClient.clients.findPermissions({
99 | id: client[0].id,
100 | name: permissionName
101 | });
102 | await kcClient.clients.delPermission({
103 | id: client[0].id,
104 | type: permissionType,
105 | permissionId: permission[0].id
106 | });
107 | return "Permission deleted successfully";
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/iam/policy.ts:
--------------------------------------------------------------------------------
1 | import KcAdminClient from '@keycloak/keycloak-admin-client';
2 | import { Injectable } from '@nestjs/common';
3 | import { ConfigService } from '@nestjs/config';
4 | import { PolicyDto } from '../dto';
5 | import { KeycloakClient } from './client';
6 |
7 |
8 |
9 | @Injectable()
10 | export class KeycloakAuthPolicy {
11 | private kcTenantAdminClient: KcAdminClient;
12 | constructor(
13 | private readonly keycloakClient: KeycloakClient,
14 | private readonly config: ConfigService
15 | ) { }
16 |
17 | public async createPolicy(body: PolicyDto, token: string): Promise {
18 | const { tenantName, clientName, policyType, policyDetails } = body;
19 | this.kcTenantAdminClient = new KcAdminClient({
20 | baseUrl: this.config.get('keycloak.server'),
21 | realmName: tenantName
22 | });
23 |
24 | const parts = token.split(' ')
25 | this.kcTenantAdminClient.setAccessToken(parts[1]);
26 |
27 | await this.kcTenantAdminClient.clients.createPolicy(
28 | {
29 | id: (await this.keycloakClient.findClient(this.kcTenantAdminClient, clientName)).id,
30 | type: policyType
31 | },
32 | policyDetails
33 | )
34 |
35 | return 'Policy created successfully';
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/iam/resource.ts:
--------------------------------------------------------------------------------
1 | import KcAdminClient from '@keycloak/keycloak-admin-client';
2 | import ClientRepresentation from '@keycloak/keycloak-admin-client/lib/defs/clientRepresentation';
3 | import ScopeRepresentation from '@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation';
4 | import { Injectable } from '@nestjs/common';
5 | import { ConfigService } from '@nestjs/config';
6 | import { ResourceDto } from '../dto';
7 | import { KeycloakClient } from './client';
8 |
9 | @Injectable()
10 | export class KeycloakAuthResource {
11 | private kcTenantAdminClient: KcAdminClient;
12 | constructor(
13 | private readonly keycloakClient: KeycloakClient,
14 | private readonly config: ConfigService
15 | ) { }
16 |
17 | public async createResource(body: ResourceDto, token: string): Promise {
18 | const { tenantName, clientName, resourceDetails } = body;
19 | this.kcTenantAdminClient = new KcAdminClient({
20 | baseUrl: this.config.get('keycloak.server'),
21 | realmName: tenantName
22 | });
23 |
24 | const parts = token.split(' ')
25 | this.kcTenantAdminClient.setAccessToken(parts[1]);
26 |
27 | const myClient: ClientRepresentation = await this.keycloakClient.findClient(this.kcTenantAdminClient, clientName);
28 |
29 | await this.kcTenantAdminClient.clients.createResource(
30 | {
31 | id: myClient.id
32 | },
33 | {
34 | ...resourceDetails,
35 | scopes: ['string' as ScopeRepresentation] // remember to replace this
36 | }
37 | )
38 |
39 | return 'Resource created successfully';
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/iam/scope.ts:
--------------------------------------------------------------------------------
1 | import KcAdminClient from '@keycloak/keycloak-admin-client';
2 | import { Injectable } from "@nestjs/common";
3 | import { ConfigService } from '@nestjs/config';
4 | import { ScopeDto } from '../dto';
5 | import { KeycloakClient } from './client';
6 |
7 | @Injectable()
8 | export class KeycloakAuthScope {
9 | private kcTenantAdminClient: KcAdminClient;
10 | constructor(
11 | private readonly keycloakClient: KeycloakClient,
12 | private readonly config: ConfigService
13 | ) { }
14 |
15 | public async createScope(body: ScopeDto, token: string): Promise {
16 | const { tenantName, clientName, scopeDetails } = body;
17 | this.kcTenantAdminClient = new KcAdminClient({
18 | baseUrl: this.config.get('keycloak.server'),
19 | realmName: tenantName
20 | });
21 |
22 | const parts = token.split(' ')
23 | this.kcTenantAdminClient.setAccessToken(parts[1]);
24 |
25 | await this.kcTenantAdminClient.clients.createAuthorizationScope(
26 | {
27 | id: (await this.keycloakClient.findClient(this.kcTenantAdminClient, clientName)).id
28 | },
29 | scopeDetails
30 | );
31 | return 'Scope created successfully';
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/logger/index.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from "@nestjs/common";
2 | import { logger } from "./logger.middleware";
3 |
4 | export function setupLogger(app: INestApplication) {
5 | app.use(logger);
6 | }
7 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/logger/logger.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express';
2 | import SystemLogger from './system.logger';
3 |
4 | export function logger(req: Request, res: Response, next: NextFunction) {
5 | const Logger = new SystemLogger('HTTP');
6 |
7 | const startAt = Date.now();
8 | const { ip, method, path: url } = req;
9 | const userAgent = req.get("user-agent") || "";
10 |
11 | res.on("finish", () => {
12 | const { statusCode } = res;
13 | const contentLength = res.get("content-length") || 0;
14 | const message = `${method} ${url} ${statusCode} ${Date.now() - startAt}ms ${contentLength} - ${userAgent} ${ip}`;
15 |
16 | if (statusCode >= 400) {
17 | return Logger.error(message);
18 | }
19 | return Logger.log(message);
20 | });
21 |
22 | next();
23 | }
24 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/logger/system.logger.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, LoggerService } from '@nestjs/common';
2 | import { Logger, createLogger, format, transports } from 'winston';
3 |
4 | enum WinstonLogLevel {
5 | INFO = 'info',
6 | ERROR = 'error',
7 | WARN = 'warn',
8 | VERBOSE = 'verbose',
9 | DEBUG = 'debug',
10 | }
11 |
12 | @Injectable()
13 | export default class SystemLogger implements LoggerService {
14 | public logger: Logger;
15 | constructor(context: string) {
16 | const { combine, timestamp, errors, label, printf } = format;
17 | const customLoggerFormat = printf(
18 | ({ level, message, label, timestamp, stack }) => {
19 | const log = `${timestamp} ${level} [${label}]: ${message}`;
20 | return stack ? `${log}\n${stack}` : log;
21 | }
22 | );
23 | this.logger = createLogger({
24 | format: combine(
25 | errors({ stack: true }),
26 | timestamp({
27 | format: 'YYYY-MM-DD HH:mm:ss'
28 | }),
29 | label({ label: context }),
30 | customLoggerFormat
31 | ),
32 | transports: [new transports.Console(), new transports.File({ filename: 'logs/app.log' })],
33 | });
34 | }
35 |
36 | log(message: any) {
37 | this.logger.log(WinstonLogLevel.INFO, message);
38 | }
39 | error(message: any) {
40 | this.logger.log(WinstonLogLevel.ERROR, message);
41 | }
42 | warn(message: any) {
43 | this.logger.log(WinstonLogLevel.WARN, message);
44 | }
45 | debug?(message: any) {
46 | this.logger.log(WinstonLogLevel.DEBUG, message);
47 | }
48 | verbose?(message: any) {
49 | this.logger.log(WinstonLogLevel.VERBOSE, message);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from '@nestjs/config';
2 | import { NestFactory } from '@nestjs/core';
3 | import { AppModule } from './app.module';
4 | import { setupLogger } from './logger';
5 | import { setupSwagger } from './swagger';
6 | import { setupCors } from './utils/cors';
7 |
8 | async function bootstrap() {
9 | const app = await NestFactory.create(AppModule);
10 |
11 | const config = app.get(ConfigService);
12 | const PORT = config.get('app.port');
13 |
14 | const envList = ['dev', 'staging', 'local', 'test'];
15 |
16 | if (envList.includes(config.get('app.env'))) {
17 | setupSwagger(app);
18 | setupCors(app);
19 | }
20 | setupLogger(app);
21 | await app.listen(PORT, () => {
22 | console.log(`Listening on ::${PORT}`);
23 | });
24 | }
25 | bootstrap();
26 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/swagger/index.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
3 | import { ConfigService } from '@nestjs/config';
4 |
5 | export const setupSwagger = (app: INestApplication) => {
6 | const config = app.get(ConfigService);
7 | const options = new DocumentBuilder()
8 | .setTitle(config.get('app.name'))
9 | .setDescription(`API Documentation for the app ${config.get('app.name')}`)
10 | .setVersion(config.get('app.version'))
11 | .addBearerAuth(
12 | {
13 | type: 'http',
14 | scheme: 'bearer',
15 | bearerFormat: 'JWT',
16 | name: 'authorization',
17 | description: 'Enter JWT token',
18 | in: 'header',
19 | }
20 | )
21 | .build();
22 | const document = SwaggerModule.createDocument(app, options);
23 | SwaggerModule.setup('api/docs', app, document);
24 | };
25 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/utils/connection.utils.ts:
--------------------------------------------------------------------------------
1 | import * as mysql from 'mysql2';
2 | import Connection from 'mysql2/typings/mysql/lib/Connection';
3 | import { DbDetailsDto } from '@app/dto';
4 |
5 | export const ConnectionUtils = {
6 | getConnection: async function (dbDetails: DbDetailsDto) {
7 | const DbConnection = mysql.createConnection({
8 | host: dbDetails.host,
9 | port: dbDetails.port,
10 | user: dbDetails.tenantName,
11 | password: dbDetails.password,
12 | database: dbDetails.dbName,
13 | });
14 |
15 | const connection = new Promise((res) => {
16 | DbConnection.connect((err) => {
17 | if (err) {
18 | res(err);
19 | }
20 | else {
21 | res({ Message: 'Database connected successfuly' });
22 | }
23 | });
24 | })
25 | return Promise.resolve(await connection);
26 | },
27 |
28 | endConnection: function (DbConnection: Connection) {
29 | DbConnection.end((err) => {
30 | if (err) {
31 | throw err;
32 | }
33 | console.log('connection ended');
34 | });
35 | },
36 | };
37 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/utils/cors.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from "@nestjs/common";
2 |
3 | export function setupCors(app: INestApplication) {
4 | app.enableCors({
5 | origin: 'http://localhost:3000',
6 | methods: ['GET', 'POST', 'PATCH', 'DELETE'],
7 | allowedHeaders: ['Content-Type', 'Authorization']
8 | })
9 | }
10 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/utils/enums/index.ts:
--------------------------------------------------------------------------------
1 | import { Permission } from "./permission.enums"
2 | import { Role } from "./roles.enums"
3 |
4 | export {
5 | Role,
6 | Permission
7 | }
8 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/utils/enums/permission.enums.ts:
--------------------------------------------------------------------------------
1 | export enum Permission {
2 | p1 = 'create',
3 | p2 = 'view',
4 | p3 = 'edit',
5 | p4 = 'delete'
6 | }
7 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/utils/enums/roles.enums.ts:
--------------------------------------------------------------------------------
1 | export enum Role {
2 | r1 = 'admin',
3 | r2 = 'tenantadmin',
4 | r3 = 'user'
5 | }
6 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/utils/httpclient.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosRequestConfig } from "axios";
2 | import { IHttpClient, IHttpClientRequestParameters } from "./interfaces/httpclient";
3 |
4 | class HttpClient implements IHttpClient {
5 | async get(parameters: IHttpClientRequestParameters): Promise {
6 | const { url, payload, headers } = parameters
7 | const options: AxiosRequestConfig = {
8 | headers: headers,
9 | params: payload
10 | }
11 |
12 | return axios.get(url, options);
13 | }
14 |
15 | async post(parameters: IHttpClientRequestParameters): Promise {
16 | const { url, payload, headers } = parameters
17 | const options: AxiosRequestConfig = {
18 | headers: headers
19 | }
20 |
21 | return axios.post(url, payload, options);
22 | }
23 | }
24 | const httpClient = new HttpClient();
25 | export default httpClient;
26 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { ConnectionUtils } from "./connection.utils";
2 | import httpClient from "./httpclient";
3 |
4 | export { httpClient, ConnectionUtils }
5 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/src/utils/interfaces/httpclient.ts:
--------------------------------------------------------------------------------
1 | export interface IHttpClient {
2 | get(parameters: IHttpClientRequestParameters): Promise
3 | post(parameters: IHttpClientRequestParameters): Promise
4 | }
5 |
6 | export interface IHttpClientRequestParameters {
7 | url: string
8 | payload?: D
9 | headers?: Record
10 | }
11 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/test/unit/cache.publicKey.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { PublicKeyCache } from '@app/auth/cache.publicKey';
3 | import * as jwksClient from "jwks-rsa";
4 |
5 | jest.mock('jsonwebtoken', () => ({
6 | decode: jest.fn().mockReturnValue({
7 | header: {
8 | kid: 'kid'
9 | },
10 | iss: 'issuer',
11 | })
12 | }));
13 |
14 | describe('Testing Public Key Caching', () => {
15 | let publicKeyCache: PublicKeyCache;
16 |
17 | beforeAll(async () => {
18 | const module: TestingModule = await Test.createTestingModule({
19 | providers: [PublicKeyCache],
20 | }).compile();
21 |
22 | publicKeyCache = module.get(PublicKeyCache);
23 | });
24 |
25 | it('Testing "getPublicKey"', async () => {
26 | const token = 'token'
27 | const mockKey = jest.spyOn(jwksClient.JwksClient.prototype, 'getSigningKey').mockImplementation(() => {
28 | return Promise.resolve({
29 | getPublicKey: jest.fn().mockReturnValue('key')
30 | })
31 | });
32 |
33 | const response = await publicKeyCache.getPublicKey(token);
34 | expect(response).toEqual('key');
35 | mockKey.mockRestore();
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/test/unit/client.spec.ts:
--------------------------------------------------------------------------------
1 | import { KeycloakClient } from '@app/iam';
2 | import KcAdminClient from '@keycloak/keycloak-admin-client';
3 | import { ConfigService } from '@nestjs/config';
4 | import { Test, TestingModule } from '@nestjs/testing';
5 |
6 | const clientName = 'test-client';
7 |
8 | jest.mock('@keycloak/keycloak-admin-client', () => {
9 | return {
10 | default: jest.fn().mockImplementation(() => {
11 | return {
12 | clients: {
13 | create: jest.fn(),
14 | find: jest.fn().mockResolvedValue([
15 | {
16 | id: 'id',
17 | clientId: clientName,
18 | }
19 | ]),
20 | generateNewClientSecret: jest.fn().mockResolvedValue({ value: 'clientSecret' })
21 | },
22 | setAccessToken: jest.fn()
23 | };
24 | })
25 | };
26 | });
27 |
28 | describe('Testing Keycloak Client', () => {
29 | let keycloakClient: KeycloakClient;
30 |
31 | beforeAll(async () => {
32 | const module: TestingModule = await Test.createTestingModule({
33 | providers: [ConfigService, KeycloakClient],
34 | }).compile();
35 |
36 | keycloakClient = module.get(KeycloakClient);
37 | });
38 |
39 | it('Testing "createClient" method', async () => {
40 | const body = {
41 | tenantName: 'string',
42 | clientDetails: {
43 | clientId: clientName,
44 | rootUrl: "www.testUrl.com",
45 | }
46 | };
47 | const token = 'Bearer token';
48 | const response = await keycloakClient.createClient(body, token);
49 | expect(response).toEqual({ clientId: clientName, clientSecret: "clientSecret" });
50 | });
51 |
52 | it('Testing "findClient" method', async () => {
53 | const mockclientName = clientName;
54 | const kcAdminClient = new KcAdminClient();
55 | const response = await keycloakClient.findClient(kcAdminClient, mockclientName);
56 | expect(response.clientId).toEqual(clientName);
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/test/unit/permission.spec.ts:
--------------------------------------------------------------------------------
1 | import { KeycloakAuthPermission, KeycloakClient } from "@app/iam";
2 | import { ConfigService } from "@nestjs/config";
3 | import { Test, TestingModule } from "@nestjs/testing";
4 |
5 | jest.mock('@keycloak/keycloak-admin-client', () => {
6 | return {
7 | default: jest.fn().mockImplementation(() => {
8 | return {
9 | users: {
10 | find: jest.fn().mockResolvedValue([
11 | {
12 | id: 'id',
13 | username: 'tenantadmin'
14 | }
15 | ]),
16 | },
17 | clients: {
18 | find: jest.fn().mockResolvedValue([
19 | {
20 | id: 'id',
21 | clientId: 'string',
22 | }
23 | ]),
24 | createPermission: jest.fn()
25 | },
26 | setAccessToken: jest.fn()
27 | };
28 | })
29 | };
30 | });
31 |
32 | describe('Testing Keycloak Authorization Permission', () => {
33 | let keycloakAuthPermission: KeycloakAuthPermission;
34 |
35 | beforeAll(async () => {
36 | const module: TestingModule = await Test.createTestingModule({
37 | providers: [KeycloakClient, ConfigService, KeycloakAuthPermission]
38 | }).compile()
39 |
40 | keycloakAuthPermission = module.get(KeycloakAuthPermission);
41 | });
42 |
43 | it('Testing "createPermission" method', async () => {
44 | const body = {
45 | tenantName: 'string',
46 | permissionType: 'user',
47 | clientName: 'string',
48 | permissionDetails: {
49 | name: "test-permission",
50 | description: "test permission description"
51 | }
52 | };
53 | const token = 'Bearer token';
54 | const response = await keycloakAuthPermission.createPermission(body, token);
55 | expect(response).toEqual('Permission created successfully');
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/test/unit/policy.spec.ts:
--------------------------------------------------------------------------------
1 | import { KeycloakAuthPolicy, KeycloakClient } from '@app/iam';
2 | import { ConfigService } from '@nestjs/config';
3 | import { Test, TestingModule } from '@nestjs/testing';
4 |
5 | jest.mock('@keycloak/keycloak-admin-client', () => {
6 | return {
7 | default: jest.fn().mockImplementation(() => {
8 | return {
9 | clients: {
10 | find: jest.fn().mockResolvedValue([
11 | {
12 | id: 'id',
13 | clientId: 'string',
14 | }
15 | ]),
16 | createPolicy: jest.fn()
17 | },
18 | setAccessToken: jest.fn()
19 | };
20 | })
21 | };
22 | });
23 |
24 | describe('Testing Keycloak Auth Policy', () => {
25 | let keycloakAuthPolicy: KeycloakAuthPolicy;
26 |
27 | beforeAll(async () => {
28 | const module: TestingModule = await Test.createTestingModule({
29 | providers: [KeycloakClient, ConfigService, KeycloakAuthPolicy],
30 | }).compile();
31 |
32 | keycloakAuthPolicy = module.get(KeycloakAuthPolicy);
33 | });
34 |
35 | it('Testing "createPolicy" method', async () => {
36 | const body = {
37 | tenantName: 'string',
38 | clientName: 'string',
39 | policyType: 'user',
40 | policyDetails: {
41 | name: "test-policy",
42 | description: "test policy description"
43 | }
44 | };
45 | const token = 'Bearer token';
46 | const response = await keycloakAuthPolicy.createPolicy(body, token);
47 | expect(response).toEqual('Policy created successfully');
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/test/unit/resource.spec.ts:
--------------------------------------------------------------------------------
1 | import { KeycloakAuthResource, KeycloakClient } from '@app/iam';
2 | import { ConfigService } from '@nestjs/config';
3 | import { Test, TestingModule } from '@nestjs/testing';
4 |
5 | jest.mock('@keycloak/keycloak-admin-client', () => {
6 | return {
7 | default: jest.fn().mockImplementation(() => {
8 | return {
9 | clients: {
10 | find: jest.fn().mockResolvedValue([
11 | {
12 | id: 'id',
13 | clientId: 'string',
14 | }
15 | ]),
16 | createResource: jest.fn()
17 | },
18 | setAccessToken: jest.fn()
19 | };
20 | })
21 | };
22 | });
23 |
24 | describe('Testing Keycloak Auth Resource', () => {
25 | let keycloakAuthResource: KeycloakAuthResource;
26 |
27 | beforeAll(async () => {
28 | const module: TestingModule = await Test.createTestingModule({
29 | providers: [KeycloakClient, ConfigService, KeycloakAuthResource],
30 | }).compile();
31 |
32 | keycloakAuthResource = module.get(KeycloakAuthResource);
33 | });
34 |
35 | it('Testing "createResource" method', async () => {
36 | const body = {
37 | tenantName: 'string',
38 | clientName: 'string',
39 | resourceDetails: {
40 | name: "test-resource",
41 | uris: ["/*"]
42 | }
43 | };
44 | const token = 'Bearer token';
45 | const response = await keycloakAuthResource.createResource(body, token);
46 | expect(response).toEqual('Resource created successfully');
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/test/unit/scope.spec.ts:
--------------------------------------------------------------------------------
1 | import { KeycloakAuthScope, KeycloakClient } from "@app/iam";
2 | import { ConfigService } from "@nestjs/config";
3 | import { Test, TestingModule } from "@nestjs/testing";
4 |
5 | jest.mock('@keycloak/keycloak-admin-client', () => {
6 | return {
7 | default: jest.fn().mockImplementation(() => {
8 | return {
9 | auth: jest.fn(),
10 | users: {
11 | find: jest.fn().mockResolvedValue([
12 | {
13 | id: 'id',
14 | username: 'tenantadmin'
15 | }
16 | ]),
17 | },
18 | clients: {
19 | find: jest.fn().mockResolvedValue([
20 | {
21 | id: 'id',
22 | clientId: 'string',
23 | }
24 | ]),
25 | createAuthorizationScope: jest.fn()
26 | },
27 | setAccessToken: jest.fn()
28 | };
29 | })
30 | };
31 | });
32 |
33 | describe('Testing Keycloak Authorization Scope', () => {
34 | let keycloakAuthScope: KeycloakAuthScope;
35 |
36 | beforeAll(async () => {
37 | const module: TestingModule = await Test.createTestingModule({
38 | providers: [KeycloakClient, ConfigService, KeycloakAuthScope]
39 | }).compile();
40 |
41 | keycloakAuthScope = module.get(KeycloakAuthScope);
42 | });
43 | it('Testing "createScope" method', async () => {
44 | const body = {
45 | tenantName: 'string',
46 | clientName: 'string',
47 | scopeDetails: {
48 | name: "test-scope",
49 | }
50 | };
51 | const token = 'Bearer token';
52 | const response = await keycloakAuthScope.createScope(body, token);
53 | expect(response).toEqual('Scope created successfully');
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/tsconfig-paths-bootstrap.js:
--------------------------------------------------------------------------------
1 | import { compilerOptions } from './tsconfig.json';
2 | import { register } from 'tsconfig-paths';
3 |
4 | const baseUrl = './dist';
5 | register({
6 | baseUrl,
7 | paths: compilerOptions.paths,
8 | });
9 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/multitenancy-rest-service/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 | "skipLibCheck": true,
15 | "paths": {
16 | "@app/*": ["src/*"]
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-rest",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "prepare": "husky install",
8 | "precommit": "lint-staged",
9 | "install:multitenancy-rest-service": "cd multitenancy-rest-service && npm install",
10 | "install:tenant-registration": "cd tenant-registration && npm install",
11 | "install:tenant-provisioning": "cd tenant-provisioning && npm install",
12 | "install:tenant-master-service": "cd tenant-master-service && npm install",
13 | "install:tenant-config-service": "cd tenant-config-service && npm install",
14 | "lint:tenant-registration": "cd tenant-registration && npm run lint",
15 | "lint:tenant-provisioning": "cd tenant-provisioning && npm run lint",
16 | "lint:tenant-master-service": "cd tenant-master-service && npm run lint",
17 | "lint:tenant-config-service": "cd tenant-config-service && npm run lint",
18 | "build:tenant-registration": "cd tenant-registration && npm run build",
19 | "build:tenant-provisioning": "cd tenant-provisioning && npm run build",
20 | "build:tenant-master-service": "cd tenant-master-service && npm run build",
21 | "build:tenant-config-service": "cd tenant-config-service && npm run build",
22 | "prebuild": "run-p lint:*",
23 | "build": "run-p build:*",
24 | "postinstall": "run-s install:*",
25 | "test:rest-service": "cd multitenancy-rest-service && npm run test",
26 | "test:tenant-registration": "cd tenant-registration && npm run test",
27 | "test:tenant-provisioning": "cd tenant-provisioning && npm run test",
28 | "test:tenant-master-service": "cd tenant-master-service && npm run test",
29 | "test:tenant-config-service": "cd tenant-config-service && npm run test",
30 | "test": "run-s test:*",
31 | "app:setup:dev": "docker-compose up",
32 | "app:setup:staging": "docker-compose -f docker-compose.yml -f ./docker/docker-compose.staging.yml up",
33 | "app:setup:prod": "docker-compose -f docker-compose.yml -f ./docker/docker-compose.prod.yml up"
34 | },
35 | "repository": {
36 | "type": "git",
37 | "url": "git+https://github.com/NeoSOFT-Technologies/node-rest.git"
38 | },
39 | "keywords": [],
40 | "author": "",
41 | "license": "ISC",
42 | "bugs": {
43 | "url": "https://github.com/NeoSOFT-Technologies/node-rest/issues"
44 | },
45 | "homepage": "https://github.com/NeoSOFT-Technologies/node-rest#readme",
46 | "devDependencies": {
47 | "@commitlint/cli": "^17.0.1",
48 | "@commitlint/config-conventional": "^17.0.0",
49 | "husky": "^8.0.1",
50 | "lint-staged": "^12.1.2",
51 | "npm-run-all": "^4.1.5"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.host.url=https://sonarcloud.io
2 | sonar.projectKey=NeoSOFT-Technologies_node-rest
3 | sonar.organization=neosoft-technologies
4 |
5 | # This is the name and version displayed in the SonarCloud UI.
6 | sonar.projectName=node-rest
7 | sonar.projectVersion=1.0
8 |
9 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
10 | sonar.sources=.
11 |
12 | # Encoding of the source code. Default is default system encoding
13 | sonar.sourceEncoding=UTF-8
--------------------------------------------------------------------------------
/tenant-config-service/.eslintignore:
--------------------------------------------------------------------------------
1 | # /node_modules/* in the project root is ignored by default
2 | # build artefacts
3 | dist/*
4 | coverage/*
5 | # data definition files
6 | **/*.d.ts
7 | # 3rd party libs
8 | /src/public/
9 | # custom definition files
10 | /src/types/
--------------------------------------------------------------------------------
/tenant-config-service/.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/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | root: true,
13 | env: {
14 | node: true,
15 | jest: true,
16 | },
17 | ignorePatterns: ['.eslintrc.js'],
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/explicit-module-boundary-types': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/tenant-config-service/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | pnpm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 | package-lock.json
14 |
15 | # OS
16 | .DS_Store
17 |
18 | # Tests
19 | /coverage
20 | /.nyc_output
21 |
22 | # IDEs and editors
23 | /.idea
24 | .project
25 | .classpath
26 | .c9/
27 | *.launch
28 | .settings/
29 | *.sublime-workspace
30 |
31 | # IDE - VSCode
32 | .vscode/*
33 | !.vscode/settings.json
34 | !.vscode/tasks.json
35 | !.vscode/launch.json
36 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/tenant-config-service/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/tenant-config-service/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12
2 |
3 | WORKDIR /usr/src/app
4 | COPY package*.json ./
5 | RUN npm install
6 | COPY . .
7 | RUN npm run build
8 |
9 | EXPOSE 8848
10 |
11 | CMD [ "npm", "run", "start" ]
--------------------------------------------------------------------------------
/tenant-config-service/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6 | [circleci-url]: https://circleci.com/gh/nestjs/nest
7 |
8 | A progressive Node.js framework for building efficient and scalable server-side applications.
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 | ## Description
18 |
19 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
20 |
21 | ## Installation
22 |
23 | ```bash
24 | $ npm install
25 | ```
26 |
27 | ## Running the app
28 |
29 | ```bash
30 | # development
31 | $ npm run start
32 |
33 | # watch mode
34 | $ npm run start:dev
35 |
36 | # production mode
37 | $ npm run start:prod
38 | ```
39 |
40 | ## Test
41 |
42 | ```bash
43 | # unit tests
44 | $ npm run test
45 |
46 | # e2e tests
47 | $ npm run test:e2e
48 |
49 | # test coverage
50 | $ npm run test:cov
51 | ```
52 |
53 | ## Support
54 |
55 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
56 |
57 | ## Stay in touch
58 |
59 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
60 | - Website - [https://nestjs.com](https://nestjs.com/)
61 | - Twitter - [@nestframework](https://twitter.com/nestframework)
62 |
63 | ## License
64 |
65 | Nest is [MIT licensed](LICENSE).
66 |
--------------------------------------------------------------------------------
/tenant-config-service/config/.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-config-service
2 | MICRO_SERVICE_PORT=8848
--------------------------------------------------------------------------------
/tenant-config-service/config/prod.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-config-service
2 | MICRO_SERVICE_PORT=8848
--------------------------------------------------------------------------------
/tenant-config-service/config/staging.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-config-service
2 | MICRO_SERVICE_PORT=8848
--------------------------------------------------------------------------------
/tenant-config-service/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/tenant-config-service/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tenant-config-service",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start",
13 | "start:dev": "nest start --watch",
14 | "start:staging": "nest start",
15 | "start:debug": "nest start --debug --watch",
16 | "start:prod": "node dist/main",
17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
18 | "test": "jest",
19 | "test:watch": "jest --watch",
20 | "test:cov": "jest --coverage",
21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
22 | "test:e2e": "jest .e2e-spec.ts$"
23 | },
24 | "dependencies": {
25 | "@nestjs/common": "^8.0.0",
26 | "@nestjs/config": "^1.1.5",
27 | "@nestjs/core": "^8.0.0",
28 | "@nestjs/microservices": "^8.2.3",
29 | "@nestjs/platform-express": "^8.0.0",
30 | "@nestjs/typeorm": "^8.0.2",
31 | "mysql2": "^2.3.3",
32 | "reflect-metadata": "^0.1.13",
33 | "rimraf": "^3.0.2",
34 | "rxjs": "^7.2.0",
35 | "typeorm": "^0.2.41"
36 | },
37 | "devDependencies": {
38 | "@nestjs/cli": "^8.0.0",
39 | "@nestjs/schematics": "^8.0.0",
40 | "@nestjs/testing": "^8.0.0",
41 | "@types/express": "^4.17.13",
42 | "@types/jest": "27.0.2",
43 | "@types/node": "^16.0.0",
44 | "@types/supertest": "^2.0.11",
45 | "@typescript-eslint/eslint-plugin": "^5.0.0",
46 | "@typescript-eslint/parser": "^5.0.0",
47 | "eslint": "^8.0.1",
48 | "eslint-config-prettier": "^8.3.0",
49 | "eslint-plugin-prettier": "^4.0.0",
50 | "jest": "^27.2.5",
51 | "prettier": "^2.3.2",
52 | "source-map-support": "^0.5.20",
53 | "supertest": "^6.1.3",
54 | "ts-jest": "^27.0.3",
55 | "ts-loader": "^9.2.3",
56 | "ts-node": "^10.0.0",
57 | "tsconfig-paths": "^3.12.0",
58 | "typescript": "^4.3.5"
59 | },
60 | "jest": {
61 | "moduleFileExtensions": [
62 | "js",
63 | "json",
64 | "ts"
65 | ],
66 | "rootDir": ".",
67 | "testRegex": ".*\\.spec\\.ts$",
68 | "transform": {
69 | "^.+\\.(t|j)s$": "ts-jest"
70 | },
71 | "collectCoverageFrom": [
72 | "**/*.(t|j)s"
73 | ],
74 | "coverageDirectory": "../coverage",
75 | "testEnvironment": "node",
76 | "moduleNameMapper": {
77 | "@app/(.*)": "/src/$1"
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/tenant-config-service/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@nestjs/common';
2 | import { MessagePattern } from '@nestjs/microservices';
3 | import { AppService } from './app.service';
4 |
5 | @Controller()
6 | export class AppController {
7 | constructor(private readonly appService: AppService) {}
8 |
9 | @MessagePattern({ cmd: 'hello-message' })
10 | getConfigTenantService(message: string) {
11 | return this.appService.getHello(message);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tenant-config-service/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { TenantConfigModule } from './tenant-config/tenant.config.module';
5 |
6 | @Module({
7 | imports: [TenantConfigModule],
8 | controllers: [AppController],
9 | providers: [AppService],
10 | })
11 | export class AppModule {}
12 |
--------------------------------------------------------------------------------
/tenant-config-service/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(message: string): string {
6 | return message;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tenant-config-service/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from '@nestjs/config';
2 | import { NestFactory } from '@nestjs/core';
3 | import { AppModule } from './app.module';
4 | import { transportOptions } from './transport/transport';
5 |
6 | async function bootstrap() {
7 | const app = await NestFactory.create(AppModule);
8 | const config = app.get(ConfigService);
9 | app.connectMicroservice(transportOptions(config));
10 | await app.startAllMicroservices();
11 | }
12 | bootstrap();
13 |
--------------------------------------------------------------------------------
/tenant-config-service/src/tenant-config/config/database.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('db', () => ({
4 | host: process.env.DB_HOST || '127.0.0.1',
5 | port: process.env.DB_PORT || 3306,
6 | username: process.env.DB_USER || 'root',
7 | password: process.env.DB_PASSWORD || 'root',
8 | database: process.env.DB_DATABASE || 'rest_api',
9 | }));
10 |
--------------------------------------------------------------------------------
/tenant-config-service/src/tenant-config/config/index.ts:
--------------------------------------------------------------------------------
1 | import db from './database';
2 | import microservice from './micro-service';
3 | export default [db, microservice];
4 |
--------------------------------------------------------------------------------
/tenant-config-service/src/tenant-config/config/micro-service.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('microservice', () => ({
4 | host: process.env.MICRO_SERVICE_HOST || 'localhost',
5 | port: process.env.MICRO_SERVICE_PORT || 8848,
6 | }));
7 |
--------------------------------------------------------------------------------
/tenant-config-service/src/tenant-config/db/database.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { ConfigModule, ConfigService } from '@nestjs/config';
4 | import { TenantConfig } from '../entities/tenant.entity';
5 |
6 | @Module({
7 | imports: [
8 | TypeOrmModule.forRootAsync({
9 | imports: [ConfigModule],
10 | inject: [ConfigService],
11 | useFactory: (config: ConfigService) => ({
12 | type: 'mysql',
13 | host: config.get('db.host'),
14 | port: config.get('db.port'),
15 | username: config.get('db.username'),
16 | password: config.get('db.password'),
17 | database: config.get('db.database'),
18 | entities: [TenantConfig],
19 | synchronize: true,
20 | }),
21 | }),
22 | ],
23 | })
24 | export class DatabaseModule {}
25 |
--------------------------------------------------------------------------------
/tenant-config-service/src/tenant-config/dto/tenant.config.dto.ts:
--------------------------------------------------------------------------------
1 | export class TenantConfigDto {
2 | tenantId: number;
3 | tenantName: string;
4 | databaseName: string;
5 | description: string;
6 | createdDateTime: string;
7 | host: string;
8 | port: number;
9 | }
10 |
--------------------------------------------------------------------------------
/tenant-config-service/src/tenant-config/entities/tenant.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2 |
3 | @Entity()
4 | export class TenantConfig {
5 | @PrimaryGeneratedColumn()
6 | id: string;
7 |
8 | @Column()
9 | tenantId: number;
10 |
11 | @Column()
12 | tenantName: string;
13 |
14 | @Column()
15 | description: string;
16 |
17 | @Column()
18 | createdDateTime: string;
19 |
20 | @Column()
21 | databaseName: string;
22 |
23 | @Column()
24 | host: string;
25 |
26 | @Column()
27 | port: number;
28 |
29 | @Column({ default: '{ max_size: 30 }' })
30 | policy: string;
31 | }
32 |
--------------------------------------------------------------------------------
/tenant-config-service/src/tenant-config/tenant.config.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@nestjs/common';
2 | import {
3 | EventPattern,
4 | MessagePattern,
5 | RpcException,
6 | } from '@nestjs/microservices';
7 | import { TenantConfigDto } from './dto/tenant.config.dto';
8 | import { TenantConfigService } from './tenant.config.service';
9 |
10 | @Controller()
11 | export class TenantConfigController {
12 | constructor(private readonly tenantConfigService: TenantConfigService) {}
13 | @EventPattern({ cmd: 'set_config' })
14 | async setConfig(tenantconfig: TenantConfigDto) {
15 | try {
16 | await this.tenantConfigService.setConfig(tenantconfig);
17 | } catch (e) {
18 | throw new RpcException(e);
19 | }
20 | }
21 |
22 | @MessagePattern({ cmd: 'get_config' })
23 | async getConfig(tenantName: string) {
24 | try {
25 | return await this.tenantConfigService.getConfig(tenantName);
26 | } catch (e) {
27 | throw new RpcException(e);
28 | }
29 | }
30 |
31 | @MessagePattern({ cmd: 'update-config' })
32 | async updateConfig({ tenantname, newdescription }) {
33 | try {
34 | return await this.tenantConfigService.updateConfig(
35 | tenantname,
36 | newdescription,
37 | );
38 | } catch (e) {
39 | throw new RpcException(e);
40 | }
41 | }
42 |
43 | @EventPattern({ cmd: 'delete-config' })
44 | async deleteConfig(tenantname: string) {
45 | try {
46 | await this.tenantConfigService.deleteConfig(tenantname);
47 | } catch (e) {
48 | throw new RpcException(e);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tenant-config-service/src/tenant-config/tenant.config.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { DatabaseModule } from './db/database.module';
4 | import { TenantConfig } from './entities/tenant.entity';
5 | import { TenantConfigController } from './tenant.config.controller';
6 | import { TenantConfigService } from './tenant.config.service';
7 | import { ConfigModule } from '@nestjs/config';
8 | import config from './config';
9 |
10 | @Module({
11 | imports: [
12 | DatabaseModule,
13 | TypeOrmModule.forFeature([TenantConfig]),
14 | ConfigModule.forRoot({
15 | envFilePath: [
16 | `${process.cwd()}/../config/.env`,
17 | `${process.cwd()}/config/.env`,
18 | ],
19 | isGlobal: true,
20 | expandVariables: true,
21 | load: config,
22 | }),
23 | ],
24 | controllers: [TenantConfigController],
25 | providers: [TenantConfigService],
26 | })
27 | export class TenantConfigModule {}
28 |
--------------------------------------------------------------------------------
/tenant-config-service/src/tenant-config/tenant.config.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger, NotFoundException } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository } from 'typeorm';
4 | import { TenantConfigDto } from './dto/tenant.config.dto';
5 | import { TenantConfig } from './entities/tenant.entity';
6 |
7 | @Injectable()
8 | export class TenantConfigService {
9 | private readonly logger: Logger;
10 | constructor(
11 | @InjectRepository(TenantConfig)
12 | private readonly configRepository: Repository,
13 | ) {
14 | this.logger = new Logger('Tenant Config Service');
15 | }
16 |
17 | async setConfig(tenantconfig: TenantConfigDto) {
18 | try {
19 | return await this.configRepository.save(tenantconfig);
20 | } catch (error) {
21 | this.logger.error(`Error while setting config: ${error}`);
22 | throw error;
23 | }
24 | }
25 |
26 | async getConfig(tenantName: string) {
27 | try {
28 | return await this.configRepository.findOneOrFail({
29 | where: {
30 | tenantName: tenantName,
31 | },
32 | });
33 | } catch (error) {
34 | this.logger.error('Incorrect Tenant name entered');
35 | throw new NotFoundException('Incorrect Tenant name entered');
36 | }
37 | }
38 |
39 | async updateConfig(tenantname: string, newdescription: string) {
40 | try {
41 | const tenant: TenantConfig = await this.configRepository.findOneOrFail({
42 | where: {
43 | tenantName: tenantname,
44 | },
45 | });
46 | await this.configRepository.update(tenant.id, {
47 | ...tenant,
48 | description: newdescription,
49 | });
50 | return 'Updated successfully';
51 | } catch (e) {
52 | this.logger.error('Tenant not found');
53 | throw new NotFoundException('Tenant not found');
54 | }
55 | }
56 |
57 | async deleteConfig(tenantname: string) {
58 | try {
59 | const tenantEntity = await this.configRepository.findOneOrFail({
60 | where: {
61 | tenantName: tenantname,
62 | },
63 | });
64 | await this.configRepository.remove(tenantEntity);
65 | return 'Deletion Successfull';
66 | } catch (error) {
67 | this.logger.error(`Error while deleting config: ${error}`);
68 | throw error;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tenant-config-service/src/transport/transport.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from '@nestjs/config';
2 | import { Transport } from '@nestjs/microservices';
3 |
4 | export const transportOptions = (config: ConfigService) => {
5 | return {
6 | transport: Transport.TCP,
7 | options: {
8 | host: config.get('microservice.host'),
9 | port: config.get('microservice.port'),
10 | },
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/tenant-config-service/test/unit/tenant.config.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { TenantConfigController } from '@app/tenant-config/tenant.config.controller';
3 | import { TenantConfigService } from '@app/tenant-config/tenant.config.service';
4 |
5 | describe('Testing Tenant Config Controller', () => {
6 | let tenantConfigController: TenantConfigController;
7 | let tenantConfigService: TenantConfigService;
8 | const mockMessage = { Message: 'Tenant Config set successfully' };
9 | const mockDeleteMessage = { Message: 'Delete Tenant successfully' };
10 | const mockTenantName = 'string';
11 | const mockTenantDetails = {
12 | id: 1,
13 | tenant_id: 1,
14 | tenant_name: 'string',
15 | description: 'string',
16 | createdDateTime: 'string',
17 | tenantDbName: 'string',
18 | host: 'string',
19 | port: 1,
20 | };
21 |
22 | const mockTenantConfigService = {
23 | setConfig: jest.fn().mockResolvedValue(mockMessage),
24 | getConfig: jest.fn().mockResolvedValue(mockTenantDetails),
25 | deleteConfig: jest.fn().mockResolvedValue(mockDeleteMessage),
26 | updateConfig: jest.fn(),
27 | };
28 |
29 | const tenantConfig = {
30 | tenantId: 1,
31 | tenantName: 'string',
32 | tenantDbName: 'string',
33 | description: 'string',
34 | databaseName: 'string',
35 | createdDateTime: 'string',
36 | host: 'string',
37 | port: 3306,
38 | };
39 | beforeAll(async () => {
40 | const module: TestingModule = await Test.createTestingModule({
41 | controllers: [TenantConfigController],
42 | providers: [TenantConfigService],
43 | })
44 | .overrideProvider(TenantConfigService)
45 | .useValue(mockTenantConfigService)
46 | .compile();
47 | tenantConfigController = module.get(
48 | TenantConfigController,
49 | );
50 | tenantConfigService = module.get(TenantConfigService);
51 | });
52 |
53 | it('Testing setConfig from Tenant Config Controller', async () => {
54 | await tenantConfigController.setConfig(tenantConfig);
55 | expect(tenantConfigService.setConfig).toHaveBeenCalledWith(tenantConfig);
56 | });
57 |
58 | it('Testing getConfig from Tenant Config Controller', async () => {
59 | expect(await tenantConfigController.getConfig(mockTenantName)).toEqual(
60 | mockTenantDetails,
61 | );
62 | });
63 |
64 | it('Testing updateConfig from Tenant Config Controller', async () => {
65 | const tenantname = 'tenantName';
66 | const newdescription = 'new Description';
67 | await tenantConfigController.updateConfig({ tenantname, newdescription });
68 | expect(tenantConfigService.updateConfig).toHaveBeenCalledWith(
69 | tenantname,
70 | newdescription,
71 | );
72 | });
73 |
74 | it('Testing deleteConfig from Tenant Config Controller', async () => {
75 | await tenantConfigController.deleteConfig(mockTenantName);
76 | expect(tenantConfigService.deleteConfig).toHaveBeenCalledWith(
77 | mockTenantName,
78 | );
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/tenant-config-service/test/unit/tenant.config.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TenantConfig } from '@app/tenant-config/entities/tenant.entity';
2 | import { TenantConfigService } from '@app/tenant-config/tenant.config.service';
3 | import { Test, TestingModule } from '@nestjs/testing';
4 | import { getRepositoryToken } from '@nestjs/typeorm';
5 |
6 | describe('Testing Config Service', () => {
7 | let tenantConfigService: TenantConfigService;
8 |
9 | const mockTenantConfigDetails = {
10 | tenantId: 1234,
11 | tenantName: 'string',
12 | tenantDbName: 'string',
13 | description: 'string',
14 | databaseName: 'string',
15 | createdDateTime: 'string',
16 | host: 'string',
17 | port: 1234,
18 | };
19 |
20 | const mockconfigRepository = {
21 | save: jest.fn().mockResolvedValue('Config saved successfully'),
22 | findOneOrFail: jest.fn().mockResolvedValue(mockTenantConfigDetails),
23 | remove: jest.fn(),
24 | update: jest.fn(),
25 | };
26 |
27 | beforeAll(async () => {
28 | const module: TestingModule = await Test.createTestingModule({
29 | providers: [
30 | TenantConfigService,
31 | {
32 | provide: getRepositoryToken(TenantConfig),
33 | useValue: mockconfigRepository,
34 | },
35 | ],
36 | }).compile();
37 |
38 | tenantConfigService = module.get(TenantConfigService);
39 | });
40 |
41 | it('Testing setConfig method of TenantConfigService', async () => {
42 | expect(
43 | await tenantConfigService.setConfig(mockTenantConfigDetails),
44 | ).toEqual('Config saved successfully');
45 | });
46 |
47 | it('Testing getConfig method of TenantConfigService', async () => {
48 | expect(await tenantConfigService.getConfig('string')).toEqual(
49 | mockTenantConfigDetails,
50 | );
51 | });
52 |
53 | it('Testing updateConfig method of TenantConfigService', async () => {
54 | expect(await tenantConfigService.updateConfig('string', 'string')).toEqual(
55 | 'Updated successfully',
56 | );
57 | });
58 |
59 | it('Testing deleteConfig method of TenantConfigService', async () => {
60 | expect(await tenantConfigService.deleteConfig('string')).toEqual(
61 | 'Deletion Successfull',
62 | );
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/tenant-config-service/tsconfig-paths-bootstrap.js:
--------------------------------------------------------------------------------
1 | import { compilerOptions } from './tsconfig.json';
2 | import { register } from 'tsconfig-paths';
3 |
4 | const baseUrl = './dist';
5 | register({
6 | baseUrl,
7 | paths: compilerOptions.paths,
8 | });
9 |
--------------------------------------------------------------------------------
/tenant-config-service/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tenant-config-service/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 | "skipLibCheck": true,
15 | "strictNullChecks": false,
16 | "noImplicitAny": false,
17 | "strictBindCallApply": false,
18 | "forceConsistentCasingInFileNames": false,
19 | "noFallthroughCasesInSwitch": false,
20 | "paths": {
21 | "@app/*": ["src/*"]
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tenant-master-service/.eslintignore:
--------------------------------------------------------------------------------
1 | # /node_modules/* in the project root is ignored by default
2 | # build artefacts
3 | dist/*
4 | coverage/*
5 | # data definition files
6 | **/*.d.ts
7 | # 3rd party libs
8 | /src/public/
9 | # custom definition files
10 | /src/types/
--------------------------------------------------------------------------------
/tenant-master-service/.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/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | root: true,
13 | env: {
14 | node: true,
15 | jest: true,
16 | },
17 | ignorePatterns: ['.eslintrc.js'],
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/explicit-module-boundary-types': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/tenant-master-service/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | pnpm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 | package-lock.json
14 |
15 | # OS
16 | .DS_Store
17 |
18 | # Tests
19 | /coverage
20 | /.nyc_output
21 |
22 | # IDEs and editors
23 | /.idea
24 | .project
25 | .classpath
26 | .c9/
27 | *.launch
28 | .settings/
29 | *.sublime-workspace
30 |
31 | # IDE - VSCode
32 | .vscode/*
33 | !.vscode/settings.json
34 | !.vscode/tasks.json
35 | !.vscode/launch.json
36 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/tenant-master-service/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/tenant-master-service/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12
2 |
3 | WORKDIR /usr/src/app
4 | COPY package*.json ./
5 | RUN npm install
6 | COPY . .
7 | RUN npm run build
8 |
9 | EXPOSE 8847
10 |
11 | CMD [ "npm", "run", "start" ]
--------------------------------------------------------------------------------
/tenant-master-service/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6 | [circleci-url]: https://circleci.com/gh/nestjs/nest
7 |
8 | A progressive Node.js framework for building efficient and scalable server-side applications.
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 | ## Description
18 |
19 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
20 |
21 | ## Installation
22 |
23 | ```bash
24 | $ npm install
25 | ```
26 |
27 | ## Running the app
28 |
29 | ```bash
30 | # development
31 | $ npm run start
32 |
33 | # watch mode
34 | $ npm run start:dev
35 |
36 | # production mode
37 | $ npm run start:prod
38 | ```
39 |
40 | ## Test
41 |
42 | ```bash
43 | # unit tests
44 | $ npm run test
45 |
46 | # e2e tests
47 | $ npm run test:e2e
48 |
49 | # test coverage
50 | $ npm run test:cov
51 | ```
52 |
53 | ## Support
54 |
55 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
56 |
57 | ## Stay in touch
58 |
59 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
60 | - Website - [https://nestjs.com](https://nestjs.com/)
61 | - Twitter - [@nestframework](https://twitter.com/nestframework)
62 |
63 | ## License
64 |
65 | Nest is [MIT licensed](LICENSE).
66 |
--------------------------------------------------------------------------------
/tenant-master-service/config/.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-master-service
2 | MICRO_SERVICE_PORT=8847
3 |
4 | CLIENT1_HOST=tenant-provisioning
5 | CLIENT2_HOST=tenant-config-service
--------------------------------------------------------------------------------
/tenant-master-service/config/prod.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-master-service
2 | MICRO_SERVICE_PORT=8847
3 |
4 | CLIENT1_HOST=tenant-provisioning
5 | CLIENT2_HOST=tenant-config-service
--------------------------------------------------------------------------------
/tenant-master-service/config/staging.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-master-service
2 | MICRO_SERVICE_PORT=8847
3 |
4 | CLIENT1_HOST=tenant-provisioning
5 | CLIENT2_HOST=tenant-config-service
--------------------------------------------------------------------------------
/tenant-master-service/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/tenant-master-service/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tenant-master-service",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start",
13 | "start:dev": "nest start --watch",
14 | "start:staging": "nest start",
15 | "start:debug": "nest start --debug --watch",
16 | "start:prod": "node dist/main",
17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
18 | "test": "jest",
19 | "test:watch": "jest --watch",
20 | "test:cov": "jest --coverage",
21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
22 | "test:e2e": "jest .e2e-spec.ts$"
23 | },
24 | "dependencies": {
25 | "@nestjs/common": "^8.0.0",
26 | "@nestjs/config": "^1.1.5",
27 | "@nestjs/core": "^8.0.0",
28 | "@nestjs/microservices": "^8.2.3",
29 | "@nestjs/platform-express": "^8.0.0",
30 | "mysql2": "^2.3.3",
31 | "reflect-metadata": "^0.1.13",
32 | "rimraf": "^3.0.2",
33 | "rxjs": "^7.2.0"
34 | },
35 | "devDependencies": {
36 | "@nestjs/cli": "^8.0.0",
37 | "@nestjs/schematics": "^8.0.0",
38 | "@nestjs/testing": "^8.0.0",
39 | "@types/express": "^4.17.13",
40 | "@types/jest": "27.0.2",
41 | "@types/mongodb": "^4.0.7",
42 | "@types/node": "^16.0.0",
43 | "@types/supertest": "^2.0.11",
44 | "@typescript-eslint/eslint-plugin": "^5.0.0",
45 | "@typescript-eslint/parser": "^5.0.0",
46 | "eslint": "^8.0.1",
47 | "eslint-config-prettier": "^8.3.0",
48 | "eslint-plugin-prettier": "^4.0.0",
49 | "jest": "^27.2.5",
50 | "prettier": "^2.3.2",
51 | "source-map-support": "^0.5.20",
52 | "supertest": "^6.1.3",
53 | "ts-jest": "^27.0.3",
54 | "ts-loader": "^9.2.3",
55 | "ts-node": "^10.0.0",
56 | "tsconfig-paths": "^3.12.0",
57 | "typescript": "^4.3.5"
58 | },
59 | "jest": {
60 | "moduleFileExtensions": [
61 | "js",
62 | "json",
63 | "ts"
64 | ],
65 | "rootDir": ".",
66 | "testRegex": ".*\\.spec\\.ts$",
67 | "transform": {
68 | "^.+\\.(t|j)s$": "ts-jest"
69 | },
70 | "collectCoverageFrom": [
71 | "**/*.(t|j)s"
72 | ],
73 | "coverageDirectory": "../coverage",
74 | "testEnvironment": "node",
75 | "moduleNameMapper": {
76 | "@app/(.*)": "/src/$1"
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/tenant-master-service/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@nestjs/common';
2 | import { AppService } from './app.service';
3 | import { MessagePattern } from '@nestjs/microservices';
4 |
5 | @Controller()
6 | export class AppController {
7 | constructor(private readonly appService: AppService) {}
8 | @MessagePattern({ cmd: 'hello-message' })
9 | getConfigTenantService(message: string) {
10 | return this.appService.getHello(message);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tenant-master-service/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { TenantMasterModule } from './tenant-master/tenant.master.module';
5 |
6 | @Module({
7 | imports: [TenantMasterModule],
8 | controllers: [AppController],
9 | providers: [AppService],
10 | })
11 | export class AppModule {}
12 |
--------------------------------------------------------------------------------
/tenant-master-service/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(message: string): string {
6 | return message;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tenant-master-service/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from '@nestjs/config';
2 | import { NestFactory } from '@nestjs/core';
3 | import { AppModule } from './app.module';
4 | import { transportOptions } from './transport/transport';
5 |
6 | async function bootstrap() {
7 | const app = await NestFactory.create(AppModule);
8 | const config = app.get(ConfigService);
9 | app.connectMicroservice(transportOptions(config));
10 | await app.startAllMicroservices();
11 | }
12 | bootstrap();
13 |
--------------------------------------------------------------------------------
/tenant-master-service/src/tenant-master/config/database.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('db', () => ({
4 | host: process.env.DB_HOST || 'localhost',
5 | port: process.env.DB_PORT || 3306,
6 | }));
7 |
--------------------------------------------------------------------------------
/tenant-master-service/src/tenant-master/config/index.ts:
--------------------------------------------------------------------------------
1 | import database from './database';
2 | import microservice from './micro-service';
3 | export default [microservice, database];
4 |
--------------------------------------------------------------------------------
/tenant-master-service/src/tenant-master/config/micro-service.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('microservice', () => ({
4 | host: process.env.MICRO_SERVICE_HOST || 'localhost',
5 | port: process.env.MICRO_SERVICE_PORT || 8847,
6 | }));
7 |
--------------------------------------------------------------------------------
/tenant-master-service/src/tenant-master/dto/tenant.details.dto.ts:
--------------------------------------------------------------------------------
1 | export class TenantDetailsDto {
2 | tenantId: string;
3 | tenantName: string;
4 | databaseName?: string;
5 | password: string;
6 | description: string;
7 | createdDateTime: string;
8 | host?: string;
9 | port?: number;
10 | }
11 |
--------------------------------------------------------------------------------
/tenant-master-service/src/tenant-master/tenant.master.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@nestjs/common';
2 | import { EventPattern } from '@nestjs/microservices';
3 | import { TenantDetailsDto } from './dto/tenant.details.dto';
4 | import { TenantMasterService } from './tenant.master.service';
5 |
6 | @Controller()
7 | export class TenantMasterController {
8 | constructor(private readonly tenantMasterService: TenantMasterService) {}
9 |
10 | @EventPattern({ cmd: 'tenant-master' })
11 | async masterTenantService(tenantDetails: TenantDetailsDto) {
12 | try {
13 | await this.tenantMasterService.masterTenantService(tenantDetails);
14 | } catch (e) {
15 | console.error(e);
16 | throw e;
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tenant-master-service/src/tenant-master/tenant.master.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule } from '@nestjs/config';
3 | import { ClientsModule, Transport } from '@nestjs/microservices';
4 | import { TenantMasterController } from './tenant.master.controller';
5 | import { TenantMasterService } from './tenant.master.service';
6 | import config from './config';
7 |
8 | @Module({
9 | imports: [
10 | ClientsModule.register([
11 | {
12 | name: 'TENANT_CONFIG_SERVICE',
13 | transport: Transport.TCP,
14 | options: {
15 | host: process.env.CLIENT2_HOST,
16 | port: 8848,
17 | },
18 | },
19 | {
20 | name: 'TENANT_PROVISION_SERVICE',
21 | transport: Transport.TCP,
22 | options: {
23 | host: process.env.CLIENT1_HOST,
24 | port: 8878,
25 | },
26 | },
27 | ]),
28 | ConfigModule.forRoot({
29 | envFilePath: [
30 | `${process.cwd()}/../config/.env`,
31 | `${process.cwd()}/config/.env`,
32 | ],
33 | isGlobal: true,
34 | expandVariables: true,
35 | load: config,
36 | }),
37 | ],
38 | controllers: [TenantMasterController],
39 | providers: [TenantMasterService],
40 | })
41 | export class TenantMasterModule {}
42 |
--------------------------------------------------------------------------------
/tenant-master-service/src/tenant-master/tenant.master.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, Logger } from '@nestjs/common';
2 | import { ConfigService } from '@nestjs/config';
3 | import { ClientProxy } from '@nestjs/microservices';
4 | import { TenantDetailsDto } from './dto/tenant.details.dto';
5 |
6 | @Injectable()
7 | export class TenantMasterService {
8 | private readonly logger: Logger;
9 | constructor(
10 | @Inject('TENANT_PROVISION_SERVICE') private readonly client1: ClientProxy,
11 | @Inject('TENANT_CONFIG_SERVICE') private readonly client2: ClientProxy,
12 | private readonly config: ConfigService,
13 | ) {
14 | this.logger = new Logger('Tenant Master Service');
15 | }
16 | async masterTenantService(tenantDetails: TenantDetailsDto) {
17 | const tenant = {
18 | tenantName: tenantDetails.tenantName,
19 | password: tenantDetails.password,
20 | databaseName: tenantDetails.databaseName,
21 | };
22 | const message = this.client1.send({ cmd: 'create-database' }, tenant);
23 | const databaseName: string = await new Promise((res) => {
24 | message.subscribe((next) => {
25 | res(next.database_name);
26 | });
27 | });
28 | const Tenantconfig: TenantDetailsDto = {
29 | ...tenantDetails,
30 | databaseName,
31 | host: this.config.get('db.host'),
32 | port: this.config.get('db.port'),
33 | };
34 | this.client2.emit({ cmd: 'set_config' }, Tenantconfig);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tenant-master-service/src/transport/transport.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from '@nestjs/config';
2 | import { Transport } from '@nestjs/microservices';
3 |
4 | export const transportOptions = (config: ConfigService) => {
5 | return {
6 | transport: Transport.TCP,
7 | options: {
8 | host: config.get('microservice.host'),
9 | port: config.get('microservice.port'),
10 | },
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/tenant-master-service/test/unit/tenant.master.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { TenantMasterController } from '@app/tenant-master/tenant.master.controller';
3 | import { TenantMasterService } from '@app/tenant-master/tenant.master.service';
4 |
5 | describe('Tenant Master Controller', () => {
6 | let tenantMasterController: TenantMasterController;
7 | let tenantMasterService: TenantMasterService;
8 | const mocktenantDetails = {
9 | tenantId: 'string',
10 | tenantName: 'string',
11 | tenantDbName: 'string',
12 | password: process.env.TEST_PASSWORD,
13 | description: 'string',
14 | createdDateTime: 'string',
15 | host: 'string',
16 | port: 123,
17 | };
18 | const mockTenantMasterService = {
19 | masterTenantService: jest.fn().mockResolvedValue(mocktenantDetails),
20 | };
21 | beforeAll(async () => {
22 | const module: TestingModule = await Test.createTestingModule({
23 | controllers: [TenantMasterController],
24 | providers: [TenantMasterService],
25 | })
26 | .overrideProvider(TenantMasterService)
27 | .useValue(mockTenantMasterService)
28 | .compile();
29 |
30 | tenantMasterController = module.get(
31 | TenantMasterController,
32 | );
33 |
34 | tenantMasterService = module.get(TenantMasterService);
35 | });
36 |
37 | it('Testing masterTenantService of Tenant Master Controller', async () => {
38 | await tenantMasterController.masterTenantService(mocktenantDetails);
39 | expect(tenantMasterService.masterTenantService).toHaveBeenCalledWith(
40 | mocktenantDetails,
41 | );
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/tenant-master-service/test/unit/tenant.master.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { of } from 'rxjs';
3 | import { TenantMasterService } from '@app/tenant-master/tenant.master.service';
4 | import { ConfigModule } from '@nestjs/config';
5 | import config from '@app/tenant-master/config';
6 |
7 | describe('Testing Tenant Master Service', () => {
8 | let tenantMasterService: TenantMasterService;
9 | const mockClient1 = {
10 | send: jest.fn().mockImplementation(() => {
11 | return of({
12 | status: 'Database created successfully',
13 | database_name: 'db-string',
14 | });
15 | }),
16 | };
17 | const mockClient2 = {
18 | emit: jest.fn(),
19 | };
20 | const mockTenantDetails = {
21 | tenantId: 'string',
22 | tenantName: 'string',
23 | tenantDbName: 'string',
24 | password: process.env.TEST_PASSWORD,
25 | description: 'string',
26 | createdDateTime: 'string',
27 | host: 'string',
28 | port: 1234,
29 | };
30 | beforeAll(async () => {
31 | const module: TestingModule = await Test.createTestingModule({
32 | imports: [
33 | ConfigModule.forRoot({
34 | envFilePath: [`${process.cwd()}/config/.env`],
35 | isGlobal: true,
36 | expandVariables: true,
37 | load: config,
38 | }),
39 | ],
40 | providers: [
41 | TenantMasterService,
42 | {
43 | provide: 'TENANT_PROVISION_SERVICE',
44 | useValue: mockClient1,
45 | },
46 | {
47 | provide: 'TENANT_CONFIG_SERVICE',
48 | useValue: mockClient2,
49 | },
50 | ],
51 | }).compile();
52 |
53 | tenantMasterService = module.get(TenantMasterService);
54 | });
55 |
56 | describe('Testing masterTenantService method', () => {
57 | beforeAll(async () => {
58 | await tenantMasterService.masterTenantService(mockTenantDetails);
59 | });
60 | it('Testing createDatabase for tenant', async () => {
61 | const provisioningDatabase = jest.spyOn(mockClient1, 'send');
62 | expect(provisioningDatabase).toHaveBeenCalled();
63 | });
64 | it('Testing setConfig of the tenant', async () => {
65 | const settingConfig = jest.spyOn(mockClient2, 'emit');
66 | expect(settingConfig).toHaveBeenCalled();
67 | });
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/tenant-master-service/tsconfig-paths-bootstrap.js:
--------------------------------------------------------------------------------
1 | import { compilerOptions } from './tsconfig.json';
2 | import { register } from 'tsconfig-paths';
3 |
4 | const baseUrl = './dist';
5 | register({
6 | baseUrl,
7 | paths: compilerOptions.paths,
8 | });
9 |
--------------------------------------------------------------------------------
/tenant-master-service/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tenant-master-service/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 | "skipLibCheck": true,
15 | "strictNullChecks": false,
16 | "noImplicitAny": false,
17 | "strictBindCallApply": false,
18 | "forceConsistentCasingInFileNames": false,
19 | "noFallthroughCasesInSwitch": false,
20 | "paths": {
21 | "@app/*": ["src/*"]
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tenant-provisioning/.eslintignore:
--------------------------------------------------------------------------------
1 | # /node_modules/* in the project root is ignored by default
2 | # build artefacts
3 | dist/*
4 | coverage/*
5 | # data definition files
6 | **/*.d.ts
7 | # 3rd party libs
8 | /src/public/
9 | # custom definition files
10 | /src/types/
--------------------------------------------------------------------------------
/tenant-provisioning/.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/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | root: true,
13 | env: {
14 | node: true,
15 | jest: true,
16 | },
17 | ignorePatterns: ['.eslintrc.js'],
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/explicit-module-boundary-types': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/tenant-provisioning/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | pnpm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 | package-lock.json
14 |
15 | # OS
16 | .DS_Store
17 |
18 | # Tests
19 | /coverage
20 | /.nyc_output
21 |
22 | # IDEs and editors
23 | /.idea
24 | .project
25 | .classpath
26 | .c9/
27 | *.launch
28 | .settings/
29 | *.sublime-workspace
30 |
31 | # IDE - VSCode
32 | .vscode/*
33 | !.vscode/settings.json
34 | !.vscode/tasks.json
35 | !.vscode/launch.json
36 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/tenant-provisioning/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/tenant-provisioning/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12
2 |
3 | WORKDIR /usr/src/app
4 | COPY package*.json ./
5 | RUN npm install
6 | COPY . .
7 | RUN npm run build
8 |
9 | EXPOSE 8878
10 |
11 | CMD [ "npm", "run", "start" ]
--------------------------------------------------------------------------------
/tenant-provisioning/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6 | [circleci-url]: https://circleci.com/gh/nestjs/nest
7 |
8 | A progressive Node.js framework for building efficient and scalable server-side applications.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
17 |
18 | ## Description
19 |
20 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
21 |
22 | ## Installation
23 |
24 | ```bash
25 | $ npm install
26 | ```
27 |
28 | ## Running the app
29 |
30 | ```bash
31 | # development
32 | $ npm run start
33 |
34 | # watch mode
35 | $ npm run start:dev
36 |
37 | # production mode
38 | $ npm run start:prod
39 | ```
40 |
41 | ## Test
42 |
43 | ```bash
44 | # unit tests
45 | $ npm run test
46 |
47 | # e2e tests
48 | $ npm run test:e2e
49 |
50 | # test coverage
51 | $ npm run test:cov
52 | ```
53 |
54 | ## Support
55 |
56 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
57 |
58 | ## Stay in touch
59 |
60 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
61 | - Website - [https://nestjs.com](https://nestjs.com/)
62 | - Twitter - [@nestframework](https://twitter.com/nestframework)
63 |
64 | ## License
65 |
66 | Nest is [MIT licensed](LICENSE).
67 |
--------------------------------------------------------------------------------
/tenant-provisioning/config/.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-provisioning
2 | MICRO_SERVICE_PORT=8878
3 |
4 | TENANT_DATABASE_HOST=database
5 | TENANT_DATABASE_PORT=3306
--------------------------------------------------------------------------------
/tenant-provisioning/config/prod.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-provisioning
2 | MICRO_SERVICE_PORT=8878
3 |
4 | TENANT_DATABASE_HOST=database
5 | TENANT_DATABASE_PORT=3306
--------------------------------------------------------------------------------
/tenant-provisioning/config/staging.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-provisioning
2 | MICRO_SERVICE_PORT=8878
3 |
4 | TENANT_DATABASE_HOST=database
5 | TENANT_DATABASE_PORT=3306
--------------------------------------------------------------------------------
/tenant-provisioning/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src","compilerOptions": {
4 | "assets": ["tenant-provisioning/scripts/*"],
5 | "watchAssets": true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tenant-provisioning/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tenant-provisioning",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start",
13 | "start:dev": "nest start --watch",
14 | "start:staging": "nest start",
15 | "start:debug": "nest start --debug --watch",
16 | "start:prod": "node dist/main",
17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
18 | "test": "jest",
19 | "test:watch": "jest --watch",
20 | "test:cov": "jest --coverage",
21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
22 | "test:e2e": "jest .e2e-spec.ts$"
23 | },
24 | "dependencies": {
25 | "@nestjs/common": "^8.0.0",
26 | "@nestjs/config": "^1.1.5",
27 | "@nestjs/core": "^8.0.0",
28 | "@nestjs/microservices": "^8.2.3",
29 | "@nestjs/platform-express": "^8.0.0",
30 | "mysql2": "^2.3.3",
31 | "reflect-metadata": "^0.1.13",
32 | "rimraf": "^3.0.2",
33 | "rxjs": "^7.2.0"
34 | },
35 | "devDependencies": {
36 | "@nestjs/cli": "^8.0.0",
37 | "@nestjs/schematics": "^8.0.0",
38 | "@nestjs/testing": "^8.0.0",
39 | "@types/express": "^4.17.13",
40 | "@types/jest": "^26.0.24",
41 | "@types/node": "^16.0.0",
42 | "@types/supertest": "^2.0.11",
43 | "@typescript-eslint/eslint-plugin": "^4.28.2",
44 | "@typescript-eslint/parser": "^4.28.2",
45 | "eslint": "^7.30.0",
46 | "eslint-config-prettier": "^8.3.0",
47 | "eslint-plugin-prettier": "^3.4.0",
48 | "jest": "27.0.6",
49 | "prettier": "^2.3.2",
50 | "supertest": "^6.1.3",
51 | "ts-jest": "^27.0.3",
52 | "ts-loader": "^9.2.3",
53 | "ts-node": "^10.0.0",
54 | "tsconfig-paths": "^3.12.0",
55 | "typescript": "^4.3.5"
56 | },
57 | "jest": {
58 | "moduleFileExtensions": [
59 | "js",
60 | "json",
61 | "ts"
62 | ],
63 | "rootDir": ".",
64 | "testRegex": ".*\\.spec\\.ts$",
65 | "transform": {
66 | "^.+\\.(t|j)s$": "ts-jest"
67 | },
68 | "collectCoverageFrom": [
69 | "**/*.(t|j)s"
70 | ],
71 | "coverageDirectory": "../coverage",
72 | "testEnvironment": "node",
73 | "moduleNameMapper": {
74 | "@app/(.*)": "/src/$1"
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@nestjs/common';
2 | import { MessagePattern } from '@nestjs/microservices';
3 | import { AppService } from './app.service';
4 |
5 | @Controller()
6 | export class AppController {
7 | constructor(private readonly appService: AppService) {}
8 |
9 | @MessagePattern({ cmd: 'hello-2' })
10 | getHello(message: string) {
11 | return this.appService.getHello(message);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { TenantprovisionModule } from './tenant-provisioning/tenantprovision.module';
5 |
6 | @Module({
7 | imports: [TenantprovisionModule],
8 | controllers: [AppController],
9 | providers: [AppService],
10 | })
11 | export class AppModule {}
12 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(message: string): string {
6 | return message;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from '@nestjs/config';
2 | import { NestFactory } from '@nestjs/core';
3 | import { AppModule } from './app.module';
4 | import { transportOptions } from './transport/transport';
5 |
6 | async function bootstrap() {
7 | const app = await NestFactory.create(AppModule);
8 | const config = app.get(ConfigService);
9 | app.connectMicroservice(transportOptions(config));
10 | await app.startAllMicroservices();
11 | }
12 | bootstrap();
13 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/config/database.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('db', () => ({
4 | host: process.env.DB_HOST || '127.0.0.1',
5 | port: process.env.DB_PORT || 3306,
6 | username: process.env.DB_USER || 'root',
7 | password: process.env.DB_PASSWORD || 'root',
8 | }));
9 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/config/index.ts:
--------------------------------------------------------------------------------
1 | import db from './database';
2 | import microservice from './micro-service';
3 | import tenantdb from './tenantdb';
4 |
5 | export default [db, microservice, tenantdb];
6 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/config/micro-service.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('microservice', () => ({
4 | host: process.env.MICRO_SERVICE_HOST || 'localhost',
5 | port: process.env.MICRO_SERVICE_PORT || 8878,
6 | }));
7 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/config/tenantdb.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('tenantdb', () => ({
4 | host: process.env.TENANT_DATABASE_HOST || '127.0.0.1',
5 | port: process.env.TENANT_DATABASE_PORT || 3306,
6 | }));
7 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/connection.utils.ts:
--------------------------------------------------------------------------------
1 | import * as mysql from 'mysql2';
2 | import { ConfigService } from '@nestjs/config';
3 | import Connection from 'mysql2/typings/mysql/lib/Connection';
4 |
5 | export const ConnectionUtils = {
6 | getConnection: function (config: ConfigService) {
7 | const DbConnection = mysql.createConnection({
8 | host: config.get('tenantdb.host'),
9 | port: config.get('tenantdb.port'),
10 | user: config.get('db.username'),
11 | password: config.get('db.password'),
12 | multipleStatements: true,
13 | });
14 | DbConnection.connect((err) => {
15 | if (err) {
16 | console.log(`Error while connecting to db server: ${err}`);
17 | throw err;
18 | }
19 | console.log('connected');
20 | });
21 |
22 | return DbConnection;
23 | },
24 |
25 | endConnection: function (DbConnection: Connection) {
26 | DbConnection.end((err) => {
27 | if (err) {
28 | console.log(`Error while ending connection from db server: ${err}`);
29 | throw err;
30 | }
31 | console.log('connection ended');
32 | });
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/dto/column.dto.ts:
--------------------------------------------------------------------------------
1 | export class ColumnDto {
2 | columnName: string;
3 | columntype: any;
4 | }
5 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/dto/provision.tenant.dto.ts:
--------------------------------------------------------------------------------
1 | export class ProvisionTenantDto {
2 | tenantName: string;
3 | password: string;
4 | databaseName: string;
5 | }
6 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/dto/provision.tenant.table.dto.ts:
--------------------------------------------------------------------------------
1 | import { ColumnDto } from './column.dto';
2 |
3 | export class ProvisionTenantTableDto {
4 | dbName: string;
5 | tableName: string;
6 | columns: ColumnDto[];
7 | }
8 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/dto/seeding-data.dto.ts:
--------------------------------------------------------------------------------
1 | export class SeedingDataeDto {
2 | dbName: string;
3 | tableName: string;
4 | columnNames: string[];
5 | values: any[];
6 | }
7 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/scripts/create-database.sql:
--------------------------------------------------------------------------------
1 | SET @db = ?;
2 | SET @user = ?;
3 | SET @password = ?;
4 | SET @createdb = CONCAT('CREATE DATABASE IF NOT EXISTS `', @db, '`;');
5 | PREPARE stmt FROM @createdb;
6 | EXECUTE stmt;
7 | DEALLOCATE PREPARE stmt;
8 | SET @createuser = CONCAT('CREATE USER IF NOT EXISTS "',@user,'"@"%" IDENTIFIED BY "',@password,'";');
9 | PREPARE stmt FROM @createuser;
10 | EXECUTE stmt;
11 | DEALLOCATE PREPARE stmt;
12 | SET @grant = CONCAT('GRANT ALL ON `',@db,'`.* TO "',@user,'"@"%";');
13 | PREPARE stmt FROM @grant;
14 | EXECUTE stmt;
15 | DEALLOCATE PREPARE stmt;
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/scripts/create-table.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE ??.?? (?? varchar(255))
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/scripts/ping.sql:
--------------------------------------------------------------------------------
1 | SHOW DATABASES LIKE ?;
2 | SHOW TABLES FROM ??;
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/scripts/seed-data.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO ??.?? (??) VALUES (?)
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/tenantprovision.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@nestjs/common';
2 | import { MessagePattern } from '@nestjs/microservices';
3 | import { ProvisionTenantDto } from './dto/provision.tenant.dto';
4 | import { ProvisionTenantTableDto } from './dto/provision.tenant.table.dto';
5 | import { SeedingDataeDto } from './dto/seeding-data.dto';
6 | import { TenantprovisionService } from './tenantprovision.service';
7 |
8 | @Controller('tenantprovision')
9 | export class TenantprovisionController {
10 | constructor(private readonly provisionService: TenantprovisionService) {}
11 |
12 | @MessagePattern({ cmd: 'create-database' })
13 | async createDatabase(tenant: ProvisionTenantDto) {
14 | try {
15 | return await this.provisionService.createDatabase(tenant);
16 | } catch (e) {
17 | return e;
18 | }
19 | }
20 |
21 | @MessagePattern({ cmd: 'create-table' })
22 | async createTable(tableDetails: ProvisionTenantTableDto) {
23 | try {
24 | return await this.provisionService.createTable(tableDetails);
25 | } catch (e) {
26 | return e;
27 | }
28 | }
29 |
30 | @MessagePattern({ cmd: 'seed' })
31 | async seedData(data: SeedingDataeDto) {
32 | try {
33 | return await this.provisionService.seed(data);
34 | } catch (e) {
35 | return e;
36 | }
37 | }
38 |
39 | @MessagePattern({ cmd: 'ping' })
40 | async ping(tenantData: ProvisionTenantDto) {
41 | try {
42 | return await this.provisionService.ping(tenantData);
43 | } catch (e) {
44 | return e;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/tenant-provisioning/tenantprovision.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TenantprovisionController } from './tenantprovision.controller';
3 | import { TenantprovisionService } from './tenantprovision.service';
4 | import { ConfigModule } from '@nestjs/config';
5 | import config from './config';
6 |
7 | @Module({
8 | imports: [
9 | ConfigModule.forRoot({
10 | envFilePath: [
11 | `${process.cwd()}/../config/.env`,
12 | `${process.cwd()}/config/.env`,
13 | ],
14 | isGlobal: true,
15 | expandVariables: true,
16 | load: config,
17 | }),
18 | ],
19 | controllers: [TenantprovisionController],
20 | providers: [TenantprovisionService],
21 | })
22 | export class TenantprovisionModule {}
23 |
--------------------------------------------------------------------------------
/tenant-provisioning/src/transport/transport.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from '@nestjs/config';
2 | import { Transport } from '@nestjs/microservices';
3 |
4 | export const transportOptions = (config: ConfigService) => {
5 | return {
6 | transport: Transport.TCP,
7 | options: {
8 | host: config.get('microservice.host'),
9 | port: config.get('microservice.port'),
10 | },
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/tenant-provisioning/test/unit/tenantprovision.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { TenantprovisionController } from '@app/tenant-provisioning/tenantprovision.controller';
3 | import { TenantprovisionService } from '@app/tenant-provisioning/tenantprovision.service';
4 |
5 | describe('Testing Provisioning MicroService Controller', () => {
6 | let tenantprovisionController: TenantprovisionController;
7 |
8 | const mockTenantprovisionService = {
9 | createDatabase: jest.fn().mockResolvedValue({
10 | Message: 'Tenant Database Provisoned successfully',
11 | }),
12 | createTable: jest
13 | .fn()
14 | .mockResolvedValue({ Message: 'Table created successfully' }),
15 | seed: jest.fn().mockResolvedValue({ Message: 'Data seeded successfully' }),
16 | ping: jest.fn().mockResolvedValue({ Message: 'Ping successfully' }),
17 | };
18 |
19 | beforeAll(async () => {
20 | const module: TestingModule = await Test.createTestingModule({
21 | controllers: [TenantprovisionController],
22 | providers: [TenantprovisionService],
23 | })
24 | .overrideProvider(TenantprovisionService)
25 | .useValue(mockTenantprovisionService)
26 | .compile();
27 |
28 | tenantprovisionController = module.get(
29 | TenantprovisionController,
30 | );
31 | });
32 |
33 | it('Testing tenantprovisionController createDatabase', async () => {
34 | const Tenant = {
35 | tenantName: 'string',
36 | password: process.env.TEST_PASSWORD,
37 | databaseName: 'string',
38 | };
39 | expect(await tenantprovisionController.createDatabase(Tenant)).toEqual({
40 | Message: 'Tenant Database Provisoned successfully',
41 | });
42 | });
43 |
44 | it('Testing tenantprovisionController createTable', async () => {
45 | const provisionTenantTable = {
46 | dbName: 'string',
47 | tableName: 'string',
48 | columns: [
49 | {
50 | columnName: 'string',
51 | columntype: 'any',
52 | },
53 | ],
54 | };
55 |
56 | expect(
57 | await tenantprovisionController.createTable(provisionTenantTable),
58 | ).toEqual({
59 | Message: 'Table created successfully',
60 | });
61 | });
62 |
63 | it('Testing tenantprovisionController seedData', async () => {
64 | const SeedingData = {
65 | dbName: 'string',
66 | tableName: 'string',
67 | columnNames: ['string'],
68 | values: ['any'],
69 | };
70 |
71 | expect(await tenantprovisionController.seedData(SeedingData)).toEqual({
72 | Message: 'Data seeded successfully',
73 | });
74 | });
75 |
76 | it('Testing tenantprovisionController ping', async () => {
77 | const TenantName = {
78 | tenantName: 'string',
79 | password: process.env.TEST_PASSWORD,
80 | databaseName: 'string',
81 | };
82 |
83 | expect(await tenantprovisionController.ping(TenantName)).toEqual({
84 | Message: 'Ping successfully',
85 | });
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/tenant-provisioning/test/unit/tenantprovision.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { ConfigModule } from '@nestjs/config';
2 | import { Test, TestingModule } from '@nestjs/testing';
3 | import config from '@app/tenant-provisioning/config';
4 | import { TenantprovisionService } from '@app/tenant-provisioning/tenantprovision.service';
5 |
6 | jest.mock('@app/tenant-provisioning/connection.utils', () => {
7 | return {
8 | ConnectionUtils: {
9 | getConnection: jest.fn(() => ({
10 | query: jest
11 | .fn()
12 | .mockImplementationOnce((arg1, arg2, cb) => {
13 | cb(null, ['Tenant-Database', 'Tenant-Table']);
14 | })
15 | .mockImplementation((arg1, arg2, cb) => {
16 | cb();
17 | }),
18 | })),
19 | endConnection: jest.fn(),
20 | },
21 | };
22 | });
23 |
24 | describe('Testing Provisioning MicroService Service', () => {
25 | let tenantprovisionService: TenantprovisionService;
26 |
27 | beforeAll(async () => {
28 | const module: TestingModule = await Test.createTestingModule({
29 | imports: [
30 | ConfigModule.forRoot({
31 | envFilePath: [`${process.cwd()}/config/.env`],
32 | isGlobal: true,
33 | expandVariables: true,
34 | load: config,
35 | }),
36 | ],
37 | providers: [TenantprovisionService],
38 | }).compile();
39 |
40 | tenantprovisionService = module.get(
41 | TenantprovisionService,
42 | );
43 | });
44 |
45 | it('Testing tenantprovisionService ping', async () => {
46 | const TenantName = {
47 | tenantName: 'string',
48 | password: process.env.TEST_PASSWORD,
49 | databaseName: 'string',
50 | };
51 |
52 | expect(await tenantprovisionService.ping(TenantName)).toEqual({
53 | 'Tenant-Database': 'Tenant-Database',
54 | 'Tenant-Table': 'Tenant-Table',
55 | });
56 | });
57 |
58 | it('Testing tenantprovisionService createDatabase', async () => {
59 | const TenantName = {
60 | tenantName: 'string',
61 | password: process.env.TEST_PASSWORD,
62 | databaseName: 'db-string',
63 | };
64 | expect(
65 | (await tenantprovisionService.createDatabase(TenantName)).status,
66 | ).toEqual('Database created successfully');
67 | });
68 |
69 | it('Testing tenantprovisionService createTable', async () => {
70 | const provisionTenantTable = {
71 | dbName: 'db-string',
72 | tableName: 'string',
73 | columns: [
74 | {
75 | columnName: 'string',
76 | columntype: 'int',
77 | },
78 | ],
79 | };
80 |
81 | expect(
82 | (await tenantprovisionService.createTable(provisionTenantTable)).status,
83 | ).toEqual('Table created successfully');
84 | });
85 |
86 | it('Testing tenantprovisionService seed', async () => {
87 | const SeedingData = {
88 | dbName: 'db-string',
89 | tableName: 'string',
90 | columnNames: ['string'],
91 | values: ['any'],
92 | };
93 |
94 | expect((await tenantprovisionService.seed(SeedingData)).status).toEqual(
95 | 'Data seeded successfully',
96 | );
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/tenant-provisioning/tsconfig-paths-bootstrap.js:
--------------------------------------------------------------------------------
1 | import { compilerOptions } from './tsconfig.json';
2 | import { register } from 'tsconfig-paths';
3 |
4 | const baseUrl = './dist';
5 | register({
6 | baseUrl,
7 | paths: compilerOptions.paths,
8 | });
9 |
--------------------------------------------------------------------------------
/tenant-provisioning/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tenant-provisioning/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 | "skipLibCheck": true,
15 | "paths": {
16 | "@app/*": ["src/*"]
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tenant-registration/.eslintignore:
--------------------------------------------------------------------------------
1 | # /node_modules/* in the project root is ignored by default
2 | # build artefacts
3 | dist/*
4 | coverage/*
5 | # data definition files
6 | **/*.d.ts
7 | # 3rd party libs
8 | /src/public/
9 | # custom definition files
10 | /src/types/
--------------------------------------------------------------------------------
/tenant-registration/.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/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | root: true,
13 | env: {
14 | node: true,
15 | jest: true,
16 | },
17 | ignorePatterns: ['.eslintrc.js'],
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/explicit-module-boundary-types': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/tenant-registration/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | pnpm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 | package-lock.json
14 |
15 | # OS
16 | .DS_Store
17 |
18 | # Tests
19 | /coverage
20 | /.nyc_output
21 |
22 | # IDEs and editors
23 | /.idea
24 | .project
25 | .classpath
26 | .c9/
27 | *.launch
28 | .settings/
29 | *.sublime-workspace
30 |
31 | # IDE - VSCode
32 | .vscode/*
33 | !.vscode/settings.json
34 | !.vscode/tasks.json
35 | !.vscode/launch.json
36 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/tenant-registration/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/tenant-registration/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12
2 |
3 | WORKDIR /usr/src/app
4 | COPY package*.json ./
5 | RUN npm install
6 | COPY . .
7 | RUN npm run build
8 |
9 | EXPOSE 8875
10 |
11 | CMD [ "npm", "run", "start" ]
--------------------------------------------------------------------------------
/tenant-registration/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6 | [circleci-url]: https://circleci.com/gh/nestjs/nest
7 |
8 | A progressive Node.js framework for building efficient and scalable server-side applications.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
17 |
18 | ## Description
19 |
20 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
21 |
22 | ## Installation
23 |
24 | ```bash
25 | $ npm install
26 | ```
27 |
28 | ## Running the app
29 |
30 | ```bash
31 | # development
32 | $ npm run start
33 |
34 | # watch mode
35 | $ npm run start:dev
36 |
37 | # production mode
38 | $ npm run start:prod
39 | ```
40 |
41 | ## Test
42 |
43 | ```bash
44 | # unit tests
45 | $ npm run test
46 |
47 | # e2e tests
48 | $ npm run test:e2e
49 |
50 | # test coverage
51 | $ npm run test:cov
52 | ```
53 |
54 | ## Support
55 |
56 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
57 |
58 | ## Stay in touch
59 |
60 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
61 | - Website - [https://nestjs.com](https://nestjs.com/)
62 | - Twitter - [@nestframework](https://twitter.com/nestframework)
63 |
64 | ## License
65 |
66 | Nest is [MIT licensed](LICENSE).
67 |
--------------------------------------------------------------------------------
/tenant-registration/config/.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-registration
2 | MICRO_SERVICE_PORT=8875
3 |
4 | CLIENT_HOST=tenant-master-service
--------------------------------------------------------------------------------
/tenant-registration/config/prod.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-registration
2 | MICRO_SERVICE_PORT=8875
3 |
4 | CLIENT_HOST=tenant-master-service
--------------------------------------------------------------------------------
/tenant-registration/config/staging.env:
--------------------------------------------------------------------------------
1 | MICRO_SERVICE_HOST=tenant-registration
2 | MICRO_SERVICE_PORT=8875
3 |
4 | CLIENT_HOST=tenant-master-service
--------------------------------------------------------------------------------
/tenant-registration/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/tenant-registration/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tenant-registration",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start",
13 | "start:dev": "nest start --watch",
14 | "start:staging": "nest start",
15 | "start:debug": "nest start --debug --watch",
16 | "start:prod": "node dist/main",
17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
18 | "test": "jest",
19 | "test:watch": "jest --watch",
20 | "test:cov": "jest --coverage",
21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
22 | "test:e2e": "jest .e2e-spec.ts$"
23 | },
24 | "dependencies": {
25 | "@nestjs/common": "^8.0.0",
26 | "@nestjs/config": "^1.1.5",
27 | "@nestjs/core": "^8.0.0",
28 | "@nestjs/microservices": "^8.2.3",
29 | "@nestjs/platform-express": "^8.0.0",
30 | "@nestjs/typeorm": "^8.0.2",
31 | "bcryptjs": "^2.4.3",
32 | "mysql2": "^2.3.3",
33 | "reflect-metadata": "^0.1.13",
34 | "rimraf": "^3.0.2",
35 | "rxjs": "^7.2.0",
36 | "typeorm": "^0.2.41"
37 | },
38 | "devDependencies": {
39 | "@nestjs/cli": "^8.0.0",
40 | "@nestjs/schematics": "^8.0.0",
41 | "@nestjs/testing": "^8.0.0",
42 | "@types/bcryptjs": "^2.4.2",
43 | "@types/express": "^4.17.13",
44 | "@types/jest": "^26.0.24",
45 | "@types/node": "^16.0.0",
46 | "@types/supertest": "^2.0.11",
47 | "@typescript-eslint/eslint-plugin": "^4.28.2",
48 | "@typescript-eslint/parser": "^4.28.2",
49 | "eslint": "^7.30.0",
50 | "eslint-config-prettier": "^8.3.0",
51 | "eslint-plugin-prettier": "^3.4.0",
52 | "jest": "27.0.6",
53 | "prettier": "^2.3.2",
54 | "supertest": "^6.1.3",
55 | "ts-jest": "^27.0.3",
56 | "ts-loader": "^9.2.3",
57 | "ts-node": "^10.0.0",
58 | "tsconfig-paths": "^3.12.0",
59 | "typescript": "^4.3.5"
60 | },
61 | "jest": {
62 | "moduleFileExtensions": [
63 | "js",
64 | "json",
65 | "ts"
66 | ],
67 | "rootDir": ".",
68 | "testRegex": ".*\\.spec\\.ts$",
69 | "transform": {
70 | "^.+\\.(t|j)s$": "ts-jest"
71 | },
72 | "collectCoverageFrom": [
73 | "**/*.(t|j)s"
74 | ],
75 | "coverageDirectory": "../coverage",
76 | "testEnvironment": "node",
77 | "moduleNameMapper": {
78 | "@app/(.*)": "/src/$1"
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tenant-registration/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from '@nestjs/common';
2 | import { MessagePattern } from '@nestjs/microservices';
3 | import { AppService } from './app.service';
4 |
5 | @Controller()
6 | export class AppController {
7 | constructor(private readonly appService: AppService) {}
8 |
9 | @MessagePattern({ cmd: 'hello' })
10 | getHello(message: string) {
11 | return this.appService.getHello(message);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tenant-registration/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { RegistertenantModule } from './tenant-reg/registertenant.module';
5 |
6 | @Module({
7 | imports: [RegistertenantModule],
8 | controllers: [AppController],
9 | providers: [AppService],
10 | })
11 | export class AppModule {}
12 |
--------------------------------------------------------------------------------
/tenant-registration/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(message: string): string {
6 | return message;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tenant-registration/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from '@nestjs/config';
2 | import { NestFactory } from '@nestjs/core';
3 | import { AppModule } from './app.module';
4 | import { transportOptions } from './transport/transport';
5 |
6 | async function bootstrap() {
7 | const app = await NestFactory.create(AppModule);
8 | const config = app.get(ConfigService);
9 | app.connectMicroservice(transportOptions(config));
10 | await app.startAllMicroservices();
11 | }
12 | bootstrap();
13 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/config/database.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('db', () => ({
4 | host: process.env.DB_HOST || 'localhost',
5 | port: process.env.DB_PORT || 3306,
6 | username: process.env.DB_USER || 'root',
7 | password: process.env.DB_PASSWORD || 'root',
8 | database: process.env.DB_DATABASE || 'rest_api',
9 | }));
10 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/config/index.ts:
--------------------------------------------------------------------------------
1 | import db from './database';
2 | import microservice from './micro-service';
3 |
4 | export default [db, microservice];
5 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/config/micro-service.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export default registerAs('microservice', () => ({
4 | host: process.env.MICRO_SERVICE_HOST || 'localhost',
5 | port: process.env.MICRO_SERVICE_PORT || 8875,
6 | }));
7 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/db/database.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule, ConfigService } from '@nestjs/config';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { Tenant } from '../entity/tenant.entity';
5 |
6 | @Module({
7 | imports: [
8 | TypeOrmModule.forRootAsync({
9 | imports: [ConfigModule],
10 | inject: [ConfigService],
11 | useFactory: (config: ConfigService) => ({
12 | type: 'mysql',
13 | host: config.get('db.host'),
14 | port: config.get('db.port'),
15 | username: config.get('db.username'),
16 | password: config.get('db.password'),
17 | database: config.get('db.database'),
18 | entities: [Tenant],
19 | synchronize: true,
20 | }),
21 | }),
22 | ],
23 | })
24 | export class DatabaseModule {}
25 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/dto/identify.tenant.dto.ts:
--------------------------------------------------------------------------------
1 | export class IdentifyTenantDto {
2 | tenantName?: string;
3 | email: string;
4 | }
5 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/dto/register.tenant.dto.ts:
--------------------------------------------------------------------------------
1 | export class RegisterTenantDto {
2 | tenantName: string;
3 | email: string;
4 | password: string;
5 | description: string;
6 | databaseName: string;
7 | databaseDescription: string;
8 | createdDateTime?: string;
9 | clientId: string;
10 | clientSecret: string;
11 | }
12 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/dto/tenant.details.dto.ts:
--------------------------------------------------------------------------------
1 | export class TenantDetailsDto {
2 | tenantId: string;
3 | tenantName: string;
4 | databaseName: string;
5 | password: string;
6 | description: string;
7 | createdDateTime: string;
8 | }
9 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/entity/tenant.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2 |
3 | @Entity()
4 | export class Tenant {
5 | @PrimaryGeneratedColumn()
6 | id: string;
7 |
8 | @Column()
9 | tenantName: string;
10 |
11 | @Column()
12 | email: string;
13 |
14 | @Column()
15 | password: string;
16 |
17 | @Column()
18 | description: string;
19 |
20 | @Column()
21 | databaseName: string;
22 |
23 | @Column()
24 | databaseDescription: string;
25 |
26 | @Column()
27 | createdDateTime: string;
28 |
29 | @Column({ default: false })
30 | isDeleted: boolean;
31 |
32 | @Column()
33 | clientId: string;
34 |
35 | @Column()
36 | clientSecret: string;
37 | }
38 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/identifier/identifier.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Repository } from 'typeorm';
4 | import { IdentifyTenantDto } from '../dto/identify.tenant.dto';
5 | import { Tenant } from '../entity/tenant.entity';
6 |
7 | @Injectable()
8 | export class IdentifierService {
9 | constructor(
10 | @InjectRepository(Tenant)
11 | private readonly tenantRepository: Repository,
12 | ) {}
13 |
14 | async identify(tenant: IdentifyTenantDto): Promise {
15 | return this.tenantRepository.count({
16 | where: {
17 | tenantName: tenant.tenantName,
18 | email: tenant.email,
19 | isDeleted: false,
20 | },
21 | });
22 | }
23 |
24 | async checkDb(dbName: string): Promise {
25 | return this.tenantRepository.count({
26 | where: {
27 | databaseName: dbName,
28 | },
29 | });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/registertenant.controller.ts:
--------------------------------------------------------------------------------
1 | import { ConflictException, Controller } from '@nestjs/common';
2 | import { MessagePattern, RpcException } from '@nestjs/microservices';
3 | import { RegisterTenantDto } from './dto/register.tenant.dto';
4 | import { IdentifierService } from './identifier/identifier.service';
5 | import { RegistertenantService } from './registertenant.service';
6 |
7 | @Controller('registertenant')
8 | export class RegistertenantController {
9 | constructor(
10 | private readonly tenantService: RegistertenantService,
11 | private readonly identifierService: IdentifierService,
12 | ) {}
13 |
14 | @MessagePattern({ cmd: 'register-tenant' })
15 | async registerTenant(tenant: RegisterTenantDto) {
16 | try {
17 | if (await this.identifierService.identify(tenant)) {
18 | return {
19 | status: 'This tenant already exists',
20 | };
21 | }
22 | return await this.tenantService.register(tenant);
23 | } catch (e) {
24 | return e;
25 | }
26 | }
27 |
28 | @MessagePattern({ cmd: 'check-dbName' })
29 | async checkDbName(dbName: string) {
30 | try {
31 | if (await this.identifierService.checkDb(dbName)) {
32 | throw new ConflictException('Database name already taken');
33 | }
34 | return true;
35 | } catch (e) {
36 | throw new RpcException(e);
37 | }
38 | }
39 |
40 | @MessagePattern({ cmd: 'get-client-id-secret' })
41 | async getClientIdSecret(tenantName: string) {
42 | try {
43 | return await this.tenantService.getIdSecret(tenantName);
44 | } catch (e) {
45 | throw new RpcException(e);
46 | }
47 | }
48 |
49 | @MessagePattern({ cmd: 'list-all-tenant' })
50 | async listAllTenant({ tenantName, isDeleted, page }) {
51 | try {
52 | return await this.tenantService.listAll(tenantName, isDeleted, page);
53 | } catch (e) {
54 | return e;
55 | }
56 | }
57 |
58 | @MessagePattern({ cmd: 'update-description' })
59 | async updateDescription({ tenantname, newdescription }) {
60 | try {
61 | return await this.tenantService.updateDescription(
62 | tenantname,
63 | newdescription,
64 | );
65 | } catch (e) {
66 | return e;
67 | }
68 | }
69 |
70 | @MessagePattern({ cmd: 'soft-delete' })
71 | async softDelete(tenantname: string) {
72 | try {
73 | return await this.tenantService.softDelete(tenantname);
74 | } catch (e) {
75 | return e;
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/registertenant.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ClientsModule, Transport } from '@nestjs/microservices';
3 | import { IdentifierService } from './identifier/identifier.service';
4 | import { RegistertenantController } from './registertenant.controller';
5 | import { DatabaseModule } from './db/database.module';
6 | import { RegistertenantService } from './registertenant.service';
7 | import { Tenant } from './entity/tenant.entity';
8 | import { TypeOrmModule } from '@nestjs/typeorm';
9 | import { ConfigModule } from '@nestjs/config';
10 | import config from './config';
11 |
12 | @Module({
13 | imports: [
14 | ClientsModule.register([
15 | {
16 | name: 'Tenant-Master',
17 | transport: Transport.TCP,
18 | options: {
19 | host: process.env.CLIENT_HOST,
20 | port: 8847,
21 | },
22 | },
23 | ]),
24 | ConfigModule.forRoot({
25 | envFilePath: [
26 | `${process.cwd()}/../config/.env`,
27 | `${process.cwd()}/config/.env`,
28 | ],
29 | isGlobal: true,
30 | expandVariables: true,
31 | load: config,
32 | }),
33 | DatabaseModule,
34 | TypeOrmModule.forFeature([Tenant]),
35 | ],
36 | controllers: [RegistertenantController],
37 | providers: [IdentifierService, RegistertenantService],
38 | })
39 | export class RegistertenantModule {}
40 |
--------------------------------------------------------------------------------
/tenant-registration/src/tenant-reg/utils/bcrypt.ts:
--------------------------------------------------------------------------------
1 | import { hashSync } from 'bcryptjs';
2 |
3 | const SALT = 10;
4 |
5 | export function encodePassword(rawPassword: string) {
6 | return hashSync(rawPassword, SALT);
7 | }
8 |
--------------------------------------------------------------------------------
/tenant-registration/src/transport/transport.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from '@nestjs/config';
2 | import { Transport } from '@nestjs/microservices';
3 |
4 | export const transportOptions = (config: ConfigService) => {
5 | return {
6 | transport: Transport.TCP,
7 | options: {
8 | host: config.get('microservice.host'),
9 | port: config.get('microservice.port'),
10 | },
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/tenant-registration/test/unit/registertenant.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { IdentifierService } from '@app/tenant-reg/identifier/identifier.service';
2 | import { RegistertenantController } from '@app/tenant-reg/registertenant.controller';
3 | import { RegistertenantService } from '@app/tenant-reg/registertenant.service';
4 | import { Test, TestingModule } from '@nestjs/testing';
5 |
6 | describe('Testing RegisTration MicroService Controller', () => {
7 | let registertenantController: RegistertenantController;
8 |
9 | const TenantDetails = {
10 | tenantName: 'string',
11 | email: 'string',
12 | password: process.env.TEST_PASSWORD,
13 | description: 'string',
14 | databaseName: 'string',
15 | databaseDescription: 'string',
16 | clientId: 'string',
17 | clientSecret: 'string',
18 | };
19 | const mockRegistertenantService = {
20 | register: jest.fn(),
21 | getIdSecret: jest.fn(),
22 | listAll: jest.fn(),
23 | updateDescription: jest.fn(),
24 | softDelete: jest.fn(),
25 | };
26 | const mockIdentifierService = {
27 | identify: jest.fn().mockResolvedValue(false),
28 | checkDb: jest.fn().mockResolvedValue(false),
29 | };
30 |
31 | beforeAll(async () => {
32 | const module: TestingModule = await Test.createTestingModule({
33 | controllers: [RegistertenantController],
34 | providers: [IdentifierService, RegistertenantService],
35 | })
36 | .overrideProvider(RegistertenantService)
37 | .useValue(mockRegistertenantService)
38 | .overrideProvider(IdentifierService)
39 | .useValue(mockIdentifierService)
40 | .compile();
41 |
42 | registertenantController = module.get(
43 | RegistertenantController,
44 | );
45 | });
46 |
47 | it('Testing registerTenantcontroller registerTenant', async () => {
48 | const mockMessage = { Message: 'Tenant Registered Successfully' };
49 | mockRegistertenantService.register.mockResolvedValue(mockMessage);
50 | expect(
51 | await registertenantController.registerTenant(TenantDetails),
52 | ).toEqual(mockMessage);
53 | });
54 |
55 | it('Testing registerTenantcontroller checkDbName', async () => {
56 | const dbName = 'string';
57 | expect(await registertenantController.checkDbName(dbName)).toEqual(true);
58 | });
59 |
60 | it('Testing registerTenantcontroller getClientIdSecret', async () => {
61 | const tenantname = 'string';
62 | const mockMessage = { Message: 'Client Id and Secret Successfully' };
63 | mockRegistertenantService.getIdSecret.mockResolvedValue(mockMessage);
64 | expect(
65 | await registertenantController.getClientIdSecret(tenantname),
66 | ).toEqual(mockMessage);
67 | });
68 |
69 | it('Testing listAllTenant', async () => {
70 | const query = {
71 | tenantName: 'tenantName',
72 | isDeleted: true,
73 | page: 1,
74 | };
75 | const mockMessage = { Message: 'All Tenant received Successfully' };
76 | mockRegistertenantService.listAll.mockResolvedValue(mockMessage);
77 | expect(await registertenantController.listAllTenant(query)).toEqual(
78 | mockMessage,
79 | );
80 | });
81 |
82 | it('Testing updateDescription', async () => {
83 | const tenantname = 'string';
84 | const newdescription = 'new description';
85 | const mockMessage = { Message: 'Tenant Updated Successfully' };
86 | mockRegistertenantService.updateDescription.mockResolvedValue(mockMessage);
87 | expect(
88 | await registertenantController.updateDescription({
89 | tenantname,
90 | newdescription,
91 | }),
92 | ).toEqual(mockMessage);
93 | });
94 |
95 | it('Testing softDelete', async () => {
96 | const tenantname = 'string';
97 | const mockMessage = { Message: 'Tenant Deleted Successfully' };
98 | mockRegistertenantService.softDelete.mockResolvedValue(mockMessage);
99 | expect(await registertenantController.softDelete(tenantname)).toEqual(
100 | mockMessage,
101 | );
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/tenant-registration/test/unit/registertenant.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Tenant } from '@app/tenant-reg/entity/tenant.entity';
2 | import { RegistertenantService } from '@app/tenant-reg/registertenant.service';
3 | import { Test, TestingModule } from '@nestjs/testing';
4 | import { getRepositoryToken } from '@nestjs/typeorm';
5 |
6 | jest.mock('bcryptjs', () => ({
7 | hashSync: jest.fn().mockReturnValue('string'),
8 | }));
9 |
10 | describe('Testing RegisTration MicroService Service', () => {
11 | let registertenantService: RegistertenantService;
12 |
13 | const tenant = {
14 | tenantName: 'string',
15 | email: 'string',
16 | password: process.env.TEST_PASSWORD,
17 | description: 'string',
18 | databaseName: 'string',
19 | databaseDescription: 'string',
20 | createdDateTime: 'string',
21 | clientId: 'string',
22 | clientSecret: 'string',
23 | };
24 | const count = 50;
25 | const mockTenantRepository = {
26 | save: jest.fn().mockResolvedValue(tenant),
27 | findAndCount: jest.fn().mockResolvedValue([[tenant], count]),
28 | findOneOrFail: jest.fn().mockResolvedValue(tenant),
29 | update: jest.fn().mockResolvedValue(tenant),
30 | };
31 | const mockClient = {
32 | emit: jest.fn(),
33 | };
34 |
35 | beforeAll(async () => {
36 | const module: TestingModule = await Test.createTestingModule({
37 | providers: [
38 | RegistertenantService,
39 | {
40 | provide: getRepositoryToken(Tenant),
41 | useValue: mockTenantRepository,
42 | },
43 | {
44 | provide: 'Tenant-Master',
45 | useValue: mockClient,
46 | },
47 | ],
48 | }).compile();
49 |
50 | registertenantService = module.get(
51 | RegistertenantService,
52 | );
53 | });
54 |
55 | it('Testing registerTenant service', async () => {
56 | const mockMessage = { Message: 'Tenant Registered Successfully' };
57 | expect(await registertenantService.register(tenant)).toEqual(mockMessage);
58 | });
59 |
60 | it('Testing getIdSecret', async () => {
61 | expect(await registertenantService.getIdSecret(tenant.tenantName)).toEqual(
62 | tenant,
63 | );
64 | });
65 |
66 | it('Testing listAll', async () => {
67 | expect(await registertenantService.listAll()).toEqual([[tenant], count]);
68 | });
69 |
70 | it('Testing updateDescription', async () => {
71 | expect(
72 | await registertenantService.updateDescription(
73 | tenant.tenantName,
74 | tenant.description,
75 | ),
76 | ).toEqual('Tenant Updated Successfully');
77 | });
78 |
79 | it('Testing softDelete', async () => {
80 | expect(await registertenantService.softDelete(tenant.tenantName)).toEqual(
81 | 'Tenant Deleted Successfully',
82 | );
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/tenant-registration/tsconfig-paths-bootstrap.js:
--------------------------------------------------------------------------------
1 | import { compilerOptions } from './tsconfig.json';
2 | import { register } from 'tsconfig-paths';
3 |
4 | const baseUrl = './dist';
5 | register({
6 | baseUrl,
7 | paths: compilerOptions.paths,
8 | });
9 |
--------------------------------------------------------------------------------
/tenant-registration/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tenant-registration/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 | "skipLibCheck": true,
15 | "paths": {
16 | "@app/*": ["src/*"]
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/wiki/docs/contribution.md:
--------------------------------------------------------------------------------
1 | # Contributing to this project
2 |
3 | Please take a moment to review this document in order to make the contribution
4 | process easy and effective for everyone involved.
5 |
6 | Following these guidelines helps to communicate that you respect the time of
7 | the developers managing and developing this open source project. In return,
8 | they should reciprocate that respect in addressing your issue or assessing
9 | patches and features.
10 |
11 |
12 | ## Using the issue tracker
13 |
14 | The issue tracker is the preferred channel for [bug reports](https://github.com/NeoSOFT-Technologies/node-rest/blob/main/wiki/docs/contribution/bug-reports.md),
15 | [features requests](https://github.com/NeoSOFT-Technologies/node-rest/blob/main/wiki/docs/contribution/feature-requests.md) and [submitting pull
16 | requests](https://github.com/NeoSOFT-Technologies/node-rest/blob/main/wiki/docs/contribution/pull-requests.md).
17 |
18 |
--------------------------------------------------------------------------------
/wiki/docs/contribution/bug-reports.md:
--------------------------------------------------------------------------------
1 | ## Bug reports
2 |
3 | A bug is a _demonstrable problem_ that is caused by the code in the repository.
4 | Good bug reports are extremely helpful - thank you!
5 |
6 | Guidelines for bug reports:
7 |
8 | 1. **Use the GitHub issue search** — check if the issue has already been
9 | reported.
10 |
11 | 2. **Check if the issue has been fixed** — try to reproduce it using the
12 | latest `main` or development branch in the repository.
13 |
14 |
15 | Example:
16 |
17 | > Short and descriptive example bug report title
18 | >
19 | > A summary of the issue and the browser/OS environment in which it occurs. If
20 | > suitable, include the steps required to reproduce the bug.
21 | >
22 | > 1. This is the first step
23 | > 2. This is the second step
24 | > 3. Further steps, etc.
25 | >
26 | > `` - a link to the reduced test case
27 | >
28 | > Any other information you want to share that is relevant to the issue being
29 | > reported. This might include the lines of code that you have identified as
30 | > causing the bug, and potential solutions (and your opinions on their
31 | > merits).
32 |
33 |
--------------------------------------------------------------------------------
/wiki/docs/contribution/feature-requests.md:
--------------------------------------------------------------------------------
1 |
2 | ## Feature requests
3 |
4 | Feature requests are welcome. But take a moment to find out whether your idea
5 | fits with the scope and aims of the project. It's up to *you* to make a strong
6 | case to convince the project's developers of the merits of this feature. Please
7 | provide as much detail and context as possible.
8 |
9 |
--------------------------------------------------------------------------------
/wiki/docs/contribution/pull-requests.md:
--------------------------------------------------------------------------------
1 |
2 | ## Pull requests
3 |
4 | Good pull requests - patches, improvements, new features - are a fantastic
5 | help. They should remain focused in scope and avoid containing unrelated
6 | commits.
7 |
8 | **Please ask first** before embarking on any significant pull request (e.g.
9 | implementing features, refactoring code, porting to a different language),
10 | otherwise you risk spending a lot of time working on something that the
11 | project's developers might not want to merge into the project.
12 |
13 | Please adhere to the coding conventions used throughout a project (indentation,
14 | accurate comments, etc.) and any other requirements (such as test coverage).
15 |
16 | Follow this process if you'd like your work considered for inclusion in the
17 | project:
18 |
19 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
20 | and configure the remotes:
21 |
22 | ```bash
23 | # Clone your fork of the repo into the current directory
24 | git clone https://github.com//
25 | # Navigate to the newly cloned directory
26 | cd
27 | # Assign the original repo to a remote called "upstream"
28 | git remote add upstream https://github.com//
29 | ```
30 |
31 | 2. If you cloned a while ago, get the latest changes from upstream:
32 |
33 | ```bash
34 | git checkout
35 | git pull upstream
36 | ```
37 |
38 | 3. Create a new topic branch (off the main project development branch) to
39 | contain your feature, change, or fix:
40 |
41 | ```bash
42 | git checkout -b
43 | ```
44 |
45 | 4. Commit your changes in logical chunks. Please adhere to these [git commit
46 | message guidelines](wiki/git-commits.md)
47 | or your code is unlikely be merged into the main project. Use Git's
48 | [interactive rebase](https://help.github.com/articles/interactive-rebase)
49 | feature to tidy up your commits before making them public.
50 |
51 | 5. Locally merge (or rebase) the upstream development branch into your topic branch:
52 |
53 | ```bash
54 | git pull [--rebase] upstream
55 | ```
56 |
57 | 6. Push your topic branch up to your fork:
58 |
59 | ```bash
60 | git push origin
61 | ```
62 |
63 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
64 | with a clear title and description.
65 |
66 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to
67 | license your work under the same license as that used by the project.
68 |
--------------------------------------------------------------------------------
/wiki/docs/coverage.md:
--------------------------------------------------------------------------------
1 | ## Test Coverage Report
2 |
3 | ### 1. Multitenancy Rest Service
4 | 
5 | \
6 | \
7 | \
8 | 
9 |
10 | ### 2. Tenant Registration
11 | 
12 | \
13 | \
14 | \
15 | 
16 | ### 3. Tenant Master Service
17 | 
18 | \
19 | \
20 | \
21 | 
22 | ### 4. Tenant Provisioning
23 | 
24 | \
25 | \
26 | \
27 | 
28 | ### 5. Tenant Config Service
29 | 
30 | \
31 | \
32 | \
33 | 
34 |
--------------------------------------------------------------------------------
/wiki/docs/keycloak.md:
--------------------------------------------------------------------------------
1 | ## Keycloak
2 |
3 | Keycloak is an open source software product to allow single sign-on with Identity and Access Management aimed at modern applications and services.
4 |
5 | ## Installation
6 | For installation we will be using docker so we need to pull the docker image. This is done using the docker-compose.yml file.
7 | ```
8 | keycloak:
9 | image: quay.io/keycloak/keycloak:16.1.0
10 | container_name: 'node_rest_keycloak'
11 | networks:
12 | - internal
13 | ports:
14 | - '8080:8080'
15 | environment:
16 | KEYCLOAK_USER: admin
17 | KEYCLOAK_PASSWORD: admin
18 | ```
19 |
20 | **Parameters Explanation**
21 | 1. `image`: Keycloak image name.
22 | 2. `KEYCLOAK_USER`: Default username.
23 | 3. `KEYCLOAK_PASSWORD`: Default password.
24 |
25 | Also make the following changes in the `.env` file.
26 | ```
27 | KEYCLOAK_SERVER=http://keycloak:8080/auth
28 | KEYCLOAK_ADMIN_USER=admin
29 | KEYCLOAK_ADMIN_PASSWORD=admin
30 | ```
31 |
32 | Once the installation is complete in order to check whether keycloak is working or not hit the following request in your browser.
33 |
34 | > REQUEST URL : http://localhost:8080
35 |
36 | Once you hit the above request the following output should be shown:
37 |
38 | 
39 |
40 | ## Usage
41 | - In order to use the Keycloak functionality we are not using the User Interface.
42 | - We will be using `keycloak-nodejs-admin-client`.
43 | - Firstly we need to install it using the following command
44 |
45 | ```
46 | $ npm install @keycloak/keycloak-admin-client
47 | ```
48 |
49 | Whenever a new Tenant is registered at that time a new Realm is created with the help of Keycloak using the following code snippet.
50 | ```
51 | await this.appService.createRealm(tenant);
52 | Here `tenant` is the Tenant Details Body.
53 | ```
54 | - This can be depicted in the form of following diagram:
55 |
56 | 
57 |
58 |
59 | Once we login to the administrative console of the Keycloak UI we get to see all the Realms that have been created as below.
60 |
61 | 
62 |
63 | - Master Realm is created during the
64 | - In order to see the contents of each Realm select any one of them and we will have the following output.
65 | - The following screen shall be displayed.
66 |
67 | 
68 |
--------------------------------------------------------------------------------
/wiki/docs/microservices/tenant-master-service.md:
--------------------------------------------------------------------------------
1 | # Tenant-Master-Service
2 |
3 | ## Introduction
4 | Tenant master service is a microservice that is written in order to keep a eye on all the microservices that are present inside the microservice boilerplate. In simple words it can be defined as a centre of communication of all the available microservices such as tenant-config service, tenant provisioning etc.
5 |
6 | ## Installation
7 | - First navigate into the tenant-master-service directory
8 | ```
9 | $ cd tenant-master-service
10 | ```
11 | - Then in order to install the dependencies from `package.json` run the following command
12 | ```
13 | $ npm install
14 | ```
15 |
16 | Once all the dependencies are installed then our microservice is ready to use and we can configure it according to our requirement.
17 |
18 | First we need to configuere the `.env` file which is present in the `config` folder in the tenant-master-service directory.
19 | ```
20 | MICRO_SERVICE_HOST=tenant-master-service
21 | MICRO_SERVICE_PORT=8847
22 | CLIENT1_HOST=tenant-provisioning
23 | CLIENT2_HOST=tenant-config-service
24 | ```
25 |
26 | `MICRO_SERVICE_HOST: `The hostname to which the specified microservice is connected
27 |
28 | `MICRO_SERVICE_PORT: `The port number to which the microservice is connected.
29 |
30 | `CLIENT1_HOST: `The name of the first client host.
31 |
32 | `CLIENT2_HOST: `The name of the second client host.
33 |
34 | ### tenant.master.service.ts
35 | ```
36 | async masterTenantService(tenantDetails: TenantDetailsDto) {
37 | const tenant = {
38 | tenantName: tenantDetails.tenantName,
39 | password: tenantDetails.password,
40 | };
41 |
42 | const message = this.client1.send({ cmd: 'create-database' }, tenant);
43 | const databaseName: string = await new Promise((res, rej) => {
44 | message.subscribe((next) => {
45 | res(next.database_name);
46 | });
47 | });
48 |
49 | const Tenantconfig: TenantDetailsDto = {
50 | ...tenantDetails,
51 | tenantDbName: databaseName,
52 | host: '127.0.0.1',
53 | port: 3306,
54 | };
55 | this.client2.emit({ cmd: 'set_config' }, Tenantconfig);
56 | ```
57 | The above piece of code tells that the tenant master service is trying to communicate with the tenant config service by sending the config details of the tenant whose database is getting created.
58 |
59 | The controller with the message handler based on request-response paradigm. This is important for this service to communicate with tenant-config microservice and tenant-provisioning microservice.
60 |
61 | ```ts
62 | # tenantprovision.module.ts
63 | @EventPattern({ cmd: 'tenant-master' })
64 | async masterTenantService(tenantDetails: TenantDetailsDto) {
65 | try {
66 | await this.tenantMasterService.masterTenantService(tenantDetails);
67 | } catch (e) {
68 | return e;
69 | }
70 | }
71 | ...
72 | ```
73 |
74 | - `@EventPattern` is used because there is only one way communication between the microservices.
75 | - While the request-response method is ideal for exchanging messages between services, it is less suitable when your message style is event-based - when you just want to publish events without waiting for a response.
76 | - In that case, you do not want the overhead required by request-response for maintaining two channels.
77 |
78 | ### Starting the microservice
79 | Since we have dockerised the microservice there is no need for explicitly starting the microservice. But if we need to start then we have to run the following command.
80 | ```bash
81 | $ npm run start
82 | ```
--------------------------------------------------------------------------------
/wiki/docs/miscellaneous/clean-docker.md:
--------------------------------------------------------------------------------
1 | # [Clean Docker Images](https://www.digitalocean.com/community/tutorials/how-to-remove-docker-images-containers-and-volumes)
2 |
3 | # How to Do a Clean Restart of a Docker Instance
4 |
5 | ### 1. Stop the container(s) using the following command
6 |
7 | `docker-compose down`
8 |
9 | ### 2. Delete all containers using the following command
10 |
11 | `docker rm -f $(docker ps -a -q)`
12 |
13 | ### 3. Delete all volumes using the following command
14 |
15 | `docker volume rm $(docker volume ls -q)`
16 |
17 | ### 4. Restart the containers using the following command
18 |
19 | `docker-compose up -d`
20 |
21 |
--------------------------------------------------------------------------------
/wiki/docs/miscellaneous/git-branch.md:
--------------------------------------------------------------------------------
1 | ### Git branch naming convention to be followed
2 | 
3 |
--------------------------------------------------------------------------------
/wiki/docs/miscellaneous/git-commits.md:
--------------------------------------------------------------------------------
1 | # Git commits and pre hooks
2 |
3 | We have added some git pre hooks while committing the code. These pre hooks are executed on every git commit.
4 |
5 | ## What is commitlint
6 |
7 | [Commitlint](https://github.com/conventional-changelog/commitlint) checks if your commit messages meet the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/).
8 |
9 | Commit message can be bifurcated into 3 section "(): "
10 |
11 | Type: can be anyone of them feat | bugfix | feature etc.
12 |
13 | > Module/Scope: Module/Scope is the name of the module you are working on
14 |
15 | Commit_Message: Actual commit message
16 |
17 | ``` git commit -m "feat(documentation): adding documentation" ```
18 |
19 | ### Type
20 |
21 | Must be one of the following:
22 |
23 | - **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
24 | - **chore**: Updating tasks etc; no production code change
25 | - **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
26 | - **docs**: Documentation only changes
27 | - **feat**: A new feature
28 | - **fix**: A bug fix
29 | - **perf**: A code change that improves performance
30 | - **refactor**: A code change that neither fixes a bug nor adds a feature
31 | - **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
32 | - **test**: Adding missing tests or correcting existing tests
33 | - **sample**: A change to the samples
34 |
--------------------------------------------------------------------------------
/wiki/docs/miscellaneous/installation-pretteri-husky-lint.md:
--------------------------------------------------------------------------------
1 | ### ESLint
2 | `npm install --save-dev eslint eslint-config-prettier eslint-plugin-import`
3 |
4 | [ESLint](https://eslint.org/) is a fully pluggable tool for identifying and reporting on patterns in JavaScript.
5 |
6 | **Configuration file**: [`.eslintrc.js`](https://github.com/smarlhens/nest7-boilerplate/blob/master/.eslintrc.js).
7 |
8 | For more configuration options and details, see the [configuration docs](https://eslint.org/docs/user-guide/configuring).
9 |
10 |
11 | ---
12 |
13 | ### Commitlint
14 |
15 | `npm install --save-dev @commitlint/cli @commitlint/config-conventional commitizen cz-conventional-changelog`
16 |
17 | [commitlint](https://github.com/conventional-changelog/commitlint) checks if your commit messages meet the [conventional commit format](https://conventionalcommits.org).
18 |
19 | **Configuration file**: [`.commitlintrc`]
20 |
21 | In general the pattern mostly looks like this:
22 |
23 | ```sh
24 | type(scope?): subject #scope is optional
25 | ```
26 |
27 | Are you a good `commitizen` ?
28 |
29 | ---
30 |
31 |
32 | ### Husky
33 |
34 | `npm install --save-dev husky lint-staged prettier`
35 |
36 | [Husky](https://github.com/typicode/husky) is a package that helps you create Git hooks easily.
37 |
38 | ---
39 |
--------------------------------------------------------------------------------
/wiki/docs/vm.md:
--------------------------------------------------------------------------------
1 | ### Installation Of Node-Rest Reposirotry in Virtual Machine
2 |
3 | Following are the steps to set-up this repository on Virtual Machine.
4 |
5 | - Install Remote Desktop Application (Eg For Linux it is Remmina) in your Machine.
6 | - Launch the remote desktop which will look like this.
7 | 
8 |
9 | - Enter the IP address.
10 | - Once the IP address is verified the following screen will appear.
11 |
12 | 
13 |
14 | - Enter the required credentials i.e admin and password, and then the VM is connected.
15 |
16 | 
17 |
18 | - Open any browser and then clone the repository of node-rest boilerplate by copy pasting the link `https://github.com/NeoSOFT-Technologies/node-rest`
19 | - Open the Terminal in the `node-rest` directory and then type the following command - `docker-compose up` and the application will start.
20 | - Open the browser and hit the following URL: `http://localhost:4004/` and the Console UI will be displayed.
21 |
22 | ---
23 |
24 | ### List of Services and Ports
25 |
26 | | Sr No. | Name of the Services | Port Number |
27 | |--------|------------------------------|-------------|
28 | | 1. | Multi-Tenancy-Rest-Service | 5000 |
29 | | 2. | Tenant Registeration Service | 8875 |
30 | | 3. | Tenant Provision Service | 8878 |
31 | | 4. | Tenant Master Service | 8847 |
32 | | 5. | Tenant Config Service | 8848 |
33 | | 6. | MySQL Database | 3306 |
34 | | 7. | Keycloak Server | 8080 |
35 | | 8. | Demo Mailer Server | 1025 & 8025 |
36 |
37 | ---
38 |
39 | ### URL List
40 |
41 | | Sr No. | Component | URL Link |
42 | |--------|-----------|----------------------------------|
43 | | 1. | Swagger | http://localhost:5000/api/docs |
44 | | 2. | Keycloak | http://localhost:8080 |
45 | | 3. | Console | http://localhost:4004 |
46 | ---
47 |
48 | ### Steps to create Tenant
49 |
50 | Follow the following steps to create the Tenant.
51 | - Hit the Console component URL in the browser which is `http://localhost:4004` and the following screen will appear.
52 |
53 | 
54 |
55 | - Enter the Admin credentials i.e Log in the application as an admin and following screen shall appear.
56 |
57 | 
58 |
59 | - On the side Menu click on Tenant and in that click on Register Tenant.
60 | - Enter the details of the Tenant that needs to be created.
61 | - Details of Tenant:
62 |
63 | |Sr No.| Tenant Details | Value |
64 | |------|-----------------------|---------------------------|
65 | | 1. | Name: | DemoTenant |
66 | | 2. | Email: | `demotenant@gmail.com` |
67 | | 3. | Password: | DemoTenant@123 |
68 | | 4. | Description: | Demo Description |
69 | | 5. | Databasename: | DemoTenantdb |
70 | | 6. | Database Description: | Demo Description |
71 |
72 | - Then the tenant is created successfully.
73 |
--------------------------------------------------------------------------------
/wiki/images/Token-Validation-Method-I.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeoSOFT-Technologies/node-rest/f5045017a46aa308e87e279be5d2d25d20f7fb3e/wiki/images/Token-Validation-Method-I.png
--------------------------------------------------------------------------------
/wiki/images/Token-validation-Method-II.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeoSOFT-Technologies/node-rest/f5045017a46aa308e87e279be5d2d25d20f7fb3e/wiki/images/Token-validation-Method-II.png
--------------------------------------------------------------------------------
/wiki/images/microservices-communication.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeoSOFT-Technologies/node-rest/f5045017a46aa308e87e279be5d2d25d20f7fb3e/wiki/images/microservices-communication.png
--------------------------------------------------------------------------------
/wiki/images/single-vs-multi-tenant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeoSOFT-Technologies/node-rest/f5045017a46aa308e87e279be5d2d25d20f7fb3e/wiki/images/single-vs-multi-tenant.png
--------------------------------------------------------------------------------
/wiki/images/swagger.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeoSOFT-Technologies/node-rest/f5045017a46aa308e87e279be5d2d25d20f7fb3e/wiki/images/swagger.PNG
--------------------------------------------------------------------------------