├── .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 | Nest Logo 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 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 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 | Nest Logo 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 | NPM Version 11 | Package License 12 | NPM Downloads 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 | Nest Logo 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 | NPM Version 11 | Package License 12 | NPM Downloads 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 | Nest Logo 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 | NPM Version 11 | Package License 12 | NPM Downloads 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 | Nest Logo 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 | NPM Version 11 | Package License 12 | NPM Downloads 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 | ![test-cases](https://user-images.githubusercontent.com/87794374/170247961-d9e1dfd5-cc98-4b73-80c9-58f9d0eb04b9.PNG) 5 | \ 6 | \ 7 | \ 8 | ![test-coverage](https://user-images.githubusercontent.com/87794374/170247991-c481c673-ae98-417d-9853-645d1d7465de.PNG) 9 | 10 | ### 2. Tenant Registration 11 | ![2 1](https://user-images.githubusercontent.com/87794374/170249292-436e6ca7-dcbe-48c5-ae07-ed25066b69d7.PNG) 12 | \ 13 | \ 14 | \ 15 | ![2](https://user-images.githubusercontent.com/87794374/170249316-e5d3b7a9-8863-4a3e-bb93-87f9639dd99f.PNG) 16 | ### 3. Tenant Master Service 17 | ![3 1](https://user-images.githubusercontent.com/87794374/170249354-79173c11-100c-41fe-b341-2333d58ea88d.PNG) 18 | \ 19 | \ 20 | \ 21 | ![3](https://user-images.githubusercontent.com/87794374/170249368-f3afb422-dea0-4171-b053-73b646b4beec.PNG) 22 | ### 4. Tenant Provisioning 23 | ![4 1](https://user-images.githubusercontent.com/87794374/170249391-578589cc-3867-4ec3-9deb-d92e08ec990d.PNG) 24 | \ 25 | \ 26 | \ 27 | ![4](https://user-images.githubusercontent.com/87794374/170249414-e1cf5118-f0ea-46d0-9eb1-79e35b0299ab.PNG) 28 | ### 5. Tenant Config Service 29 | ![5 1](https://user-images.githubusercontent.com/87794374/170249442-fe6c67eb-d736-483f-9897-f160b5a05ddd.PNG) 30 | \ 31 | \ 32 | \ 33 | ![5](https://user-images.githubusercontent.com/87794374/170249468-647456f6-3518-47d1-9ccc-003dd33152b1.PNG) 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 | ![Selection_140](https://user-images.githubusercontent.com/87708447/152768300-45fe789c-a559-41ed-80d0-3546ca9f91e9.png) 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 | ![Selection_141](https://user-images.githubusercontent.com/87708447/152771271-b786490e-a247-4d44-96f5-a1f72286ef4b.png) 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 | ![Selection_143](https://user-images.githubusercontent.com/87708447/152772046-b20762c4-050c-455d-986b-de020f64d064.png) 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 | ![Selection_144](https://user-images.githubusercontent.com/87708447/152784691-e19a32d9-caf6-471f-b9e4-5f3bff8b30cc.png) 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 | ![Git Branching@2x](https://user-images.githubusercontent.com/87794374/167783594-52c3d6ef-8810-41f9-903d-4002521784fb.png) 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 | ![Selection_177](https://user-images.githubusercontent.com/87708447/165104970-55cb2753-a814-42fb-a8ae-d3e59d309d01.png) 8 | 9 | - Enter the IP address. 10 | - Once the IP address is verified the following screen will appear. 11 | 12 | ![Selection_174](https://user-images.githubusercontent.com/87708447/165546977-4878a6ef-cad5-462c-8e28-cddda9408101.png) 13 | 14 | - Enter the required credentials i.e admin and password, and then the VM is connected. 15 | 16 | ![Selection_175](https://user-images.githubusercontent.com/87708447/165547051-bbf9c055-e58b-4dca-8f96-8c2230c2e374.png) 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 | ![Selection_177](https://user-images.githubusercontent.com/87708447/165547359-556d119d-adaa-4322-b687-7b3c879f8a90.png) 54 | 55 | - Enter the Admin credentials i.e Log in the application as an admin and following screen shall appear. 56 | 57 | ![Selection_178](https://user-images.githubusercontent.com/87708447/165547480-0afe98b1-58b7-415b-850b-625459442e13.png) 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 --------------------------------------------------------------------------------