├── .dockerignore ├── .env.example ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── --bug-report.yaml │ └── --feature-request.yaml ├── SECURITY.md ├── labeler.yml ├── pull_request_template.md └── workflows │ ├── development.yml │ ├── pr-labeler.yml │ ├── pr-title-check.yaml │ ├── release-v1.yml │ ├── release-v2.yml │ ├── reusable-release-deploy.yml │ ├── sonarqube.yml │ └── stale.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.json ├── .prettierrc ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── deploymentManifests ├── deployment.yml ├── release-v0.yml ├── release-v1.yml └── release-v2.yml ├── docker-compose.yml ├── docs ├── CODEOWNERS.md ├── CONTRIBUTING.md └── ENV.md ├── jest.config.ts ├── migrations ├── ai-model.migration.ts ├── create-test-user.migration.ts ├── hub.migration.ts ├── update-test-flow-model.migration.ts └── update-workspaceType.migration.ts ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package.json ├── pnpm-lock.yaml ├── sonar-project.properties ├── src ├── main.ts ├── migration-chatbotstats.ts ├── migration.ts ├── migrationTestflow.ts ├── migrationWorkspaceType.ts └── modules │ ├── app │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.repository.ts │ ├── app.roles.ts │ ├── app.service.ts │ ├── middleware │ │ └── metrics.middleware.ts │ └── payloads │ │ ├── curl.payload.ts │ │ ├── subscribe.payload.ts │ │ └── updaterJson.payload.ts │ ├── common │ ├── common.module.ts │ ├── config │ │ ├── configuration.ts │ │ └── env.validation.ts │ ├── decorators │ │ └── roles.decorators.ts │ ├── enum │ │ ├── ai-services.enum.ts │ │ ├── database.collection.enum.ts │ │ ├── error-messages.enum.ts │ │ ├── feedback.enum.ts │ │ ├── httpStatusCode.enum.ts │ │ ├── roles.enum.ts │ │ ├── subscription.enum.ts │ │ ├── topic.enum.ts │ │ └── updates.enum.ts │ ├── exception │ │ └── logging.exception-filter.ts │ ├── guards │ │ ├── google-oauth.guard.ts │ │ ├── jwt-auth.guard.ts │ │ ├── refresh-token.guard.ts │ │ └── roles.guard.ts │ ├── instructions │ │ └── prompt.ts │ ├── models │ │ ├── ai-log.model.ts │ │ ├── branch.model.ts │ │ ├── chatbot-stats.model.ts │ │ ├── collection.model.ts │ │ ├── collection.rxdb.model.ts │ │ ├── environment.model.ts │ │ ├── feature.model.ts │ │ ├── feedback.model.ts │ │ ├── openapi20.model.ts │ │ ├── openapi303.model.ts │ │ ├── team.model.ts │ │ ├── testflow.model.ts │ │ ├── updates.model.ts │ │ ├── user-invites.model.ts │ │ ├── user.model.ts │ │ └── workspace.model.ts │ ├── services │ │ ├── api-response.service.ts │ │ ├── blobStorage.service.ts │ │ ├── context.service.ts │ │ ├── email.service.ts │ │ ├── helper │ │ │ ├── oapi2.transformer.ts │ │ │ ├── oapi3.transformer.ts │ │ │ ├── parser.helper.ts │ │ │ └── postman.parser.ts │ │ ├── insights.service.ts │ │ ├── instrument.service.ts │ │ ├── kafka │ │ │ ├── consumer.interface.ts │ │ │ ├── consumer.service.ts │ │ │ ├── kafkajs.consumer.ts │ │ │ ├── kafkajs.producer.ts │ │ │ ├── producer.interface.ts │ │ │ └── producer.service.ts │ │ ├── parser.service.ts │ │ └── postman.parser.service.ts │ └── util │ │ ├── email.parser.util.ts │ │ └── sleep.util.ts │ ├── identity │ ├── controllers │ │ ├── auth.controller.ts │ │ ├── team.controller.ts │ │ ├── test │ │ │ ├── auth.controller.spec.ts │ │ │ └── mockData │ │ │ │ └── auth.payload.ts │ │ └── user.controller.ts │ ├── identity.module.ts │ ├── payloads │ │ ├── jwt.payload.ts │ │ ├── login.payload.ts │ │ ├── register.payload.ts │ │ ├── resetPassword.payload.ts │ │ ├── team.payload.ts │ │ ├── teamUser.payload.ts │ │ ├── user.payload.ts │ │ ├── userInvites.payload.ts │ │ └── verification.payload.ts │ ├── repositories │ │ ├── team.repository.ts │ │ ├── user.repository.ts │ │ └── userInvites.repository.ts │ ├── services │ │ ├── auth.service.ts │ │ ├── hubspot.service.ts │ │ ├── team-user.service.ts │ │ ├── team.service.ts │ │ └── user.service.ts │ └── strategies │ │ ├── google.strategy.ts │ │ ├── jwt.strategy.ts │ │ └── refresh-token.strategy.ts │ ├── proxy │ ├── adapters │ │ └── custom-websocket-adapter.ts │ ├── controllers │ │ └── http.controller.ts │ ├── gateway │ │ ├── socketio.gateway.ts │ │ └── websocket.gateway.ts │ ├── proxy.module.ts │ └── services │ │ ├── http.service.ts │ │ ├── socketio.service.ts │ │ └── websocket.service.ts │ ├── user-admin │ ├── controllers │ │ ├── user-admin.auth.controller.ts │ │ ├── user-admin.enterprise-user.controller.ts │ │ ├── user-admin.hubs.controller.ts │ │ ├── user-admin.members.controller.ts │ │ └── user-admin.workspace.controller.ts │ ├── payloads │ │ ├── auth.payload.ts │ │ ├── members.payload.ts │ │ └── workspace.payload.ts │ ├── repositories │ │ ├── user-admin.auth.repository.ts │ │ ├── user-admin.hubs.repository.ts │ │ ├── user-admin.members.repository.ts │ │ ├── user-admin.updates.repository.ts │ │ └── user-admin.workspace.repository.ts │ ├── services │ │ ├── user-admin.auth.service.ts │ │ ├── user-admin.enterprise-user.service.ts │ │ ├── user-admin.hubs.service.ts │ │ ├── user-admin.members.service.ts │ │ └── user-admin.workspace.service.ts │ └── user-admin.module.ts │ ├── views │ ├── addTeamAdminEmail.handlebars │ ├── demoteEditorEmail.handlebars │ ├── demoteTeamAdminEmail.handlebars │ ├── expireInviteEmail.handlebars │ ├── inviteTeamEmail.handlebars │ ├── inviteWorkspaceEmail.handlebars │ ├── layouts │ │ └── main.handlebars │ ├── leaveTeamEmail.handlebars │ ├── magicCodeEmail.handlebars │ ├── newOwnerEmail.handlebars │ ├── newWorkspaceEmail.handlebars │ ├── oldOwnerEmail.handlebars │ ├── partials │ │ ├── footer.handlebars │ │ └── header.handlebars │ ├── promoteViewerEmail.handlebars │ ├── removeUserEmail.handlebars │ ├── signUpEmail.handlebars │ ├── signUpVerifyEmail.handlebars │ ├── teamInviteNonRegisteredReciever.handlebars │ ├── teamInviteRegisteredReciever.handlebars │ ├── verifyEmail.handlebars │ └── welcomeEmail.handlebars │ └── workspace │ ├── controllers │ ├── ai-assistant.controller.ts │ ├── ai-assistant.gateway.ts │ ├── chatbot-stats.controller.ts │ ├── collection.controller.spec.ts │ ├── collection.controller.ts │ ├── environment.controller.ts │ ├── feature.controller.ts │ ├── feedback.controller.ts │ ├── mock-server.controller.ts │ ├── testflow.controller.ts │ ├── updates.controller.ts │ └── workspace.controller.ts │ ├── handlers │ ├── addUser.handler.ts │ ├── ai-log.handler.ts │ ├── chatbot-token.handler.ts │ ├── demoteAdmin.handlers.ts │ ├── promoteAdmin.handlers.ts │ ├── removeUser.handler.ts │ ├── teamUpdated.handler.ts │ ├── updates.handler.ts │ └── workspace.handler.ts │ ├── payloads │ ├── ai-assistant.payload.ts │ ├── ai-log.payload.ts │ ├── branch.payload.ts │ ├── chatbot-stats.payload.ts │ ├── collection.payload.ts │ ├── collectionRequest.payload.ts │ ├── environment.payload.ts │ ├── feature.payload.ts │ ├── feedback.payload.ts │ ├── mock-server.payload.ts │ ├── testflow.payload.ts │ ├── updates.payload.ts │ ├── workspace.payload.ts │ └── workspaceUser.payload.ts │ ├── repositories │ ├── ai-assistant.repository.ts │ ├── ai-log.repository.ts │ ├── branch.repository.ts │ ├── chatbot-stats.repositoy.ts │ ├── collection.repository.ts │ ├── environment.repository.ts │ ├── feature.repository.ts │ ├── feedback.repository.ts │ ├── testflow.repository.ts │ ├── updates.repository.ts │ └── workspace.repository.ts │ ├── services │ ├── ai-assistant.service.ts │ ├── ai-log.service.ts │ ├── branch.service.ts │ ├── chatbot-stats.service.ts │ ├── collection-request.service.ts │ ├── collection.service.ts │ ├── environment.service.ts │ ├── feature.service.ts │ ├── feedback.service.ts │ ├── mock-server.service.ts │ ├── testflow.service.ts │ ├── updates.service.ts │ ├── workspace-user.service.ts │ └── workspace.service.ts │ └── workspace.module.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── tsconfig.spec.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | parserOptions: { 4 | project: "tsconfig.json", 5 | tsconfigRootDir: __dirname, 6 | sourceType: "module", 7 | }, 8 | plugins: ["@typescript-eslint/eslint-plugin"], 9 | extends: [ 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended", 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: [".eslintrc.js"], 19 | rules: { 20 | "@typescript-eslint/interface-name-prefix": "off", 21 | "@typescript-eslint/explicit-function-return-type": "off", 22 | "@typescript-eslint/explicit-module-boundary-types": "off", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report to help us improve Sparrow 3 | title: "[bug]: " 4 | labels: [bug] 5 | assignees: [LordNayan] 6 | type: Bug 7 | projects: [Sparrow] 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Thank you for taking your time out to fill this bug report. 13 | - type: checkboxes 14 | attributes: 15 | label: Is there an existing issue for this? 16 | description: Please search to see if an issue already exists for the bug you encountered 17 | options: 18 | - label: I have searched the existing issues 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Description 23 | description: A brief description of what you are experiencing (Optional) 24 | placeholder: | 25 | Provide a brief description on the issue. For example: When I do , happens and I see the error as below 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: Expected Result 31 | description: What is the expected result for this issue? 32 | placeholder: | 33 | I expect to happen in place of or This is what is the expected behavior 34 | validations: 35 | required: true 36 | - type: textarea 37 | attributes: 38 | label: Actual Result 39 | description: What is the actual result which you noticed which needs a fix? 40 | placeholder: | 41 | behavior is happening in place of or This is what is happening currently in the application 42 | validations: 43 | required: true 44 | - type: textarea 45 | attributes: 46 | label: Steps to reproduce 47 | description: Add steps to reproduce this behaviour, include console or network logs and screenshots in case possible 48 | placeholder: | 49 | 1. Go to '...' 50 | 2. Click on '....' 51 | 3. Scroll down to '....' 52 | 4. See error 53 | validations: 54 | required: true 55 | - type: dropdown 56 | id: os 57 | attributes: 58 | label: Operating System 59 | options: 60 | - Windows 10 61 | - Windows 11 62 | - Mac - Intel 63 | - Mac - M1 64 | - Others 65 | validations: 66 | required: true 67 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a feature to improve Sparrow 3 | title: "[feature]: " 4 | labels: [feature] 5 | assignees: [LordNayan] 6 | type: Feature 7 | projects: [Sparrow] 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Thank you for taking the time to request a feature for Sparrow 13 | - type: checkboxes 14 | attributes: 15 | label: Is there an existing issue for this? 16 | description: Please search to see if an issue related to this feature request already exists 17 | options: 18 | - label: I have searched the existing issues 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Summary 23 | description: One paragraph description of the feature 24 | validations: 25 | required: true 26 | - type: textarea 27 | attributes: 28 | label: Why should this be worked on? 29 | description: A concise description of the problems or use cases for this feature request 30 | validations: 31 | required: true 32 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.x.x | :white_check_mark: | 8 | | < 1.0 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | If you discover a security vulnerability within Sparrow, please send an email to security@sparrowapp.dev. All security vulnerabilities will be promptly addressed. 13 | 14 | Please do not report security vulnerabilities through public GitHub issues. 15 | 16 | ## Security Update Process 17 | 18 | 1. The security team will acknowledge receipt of your vulnerability report 19 | 2. We will confirm the vulnerability and determine its impact 20 | 3. We will develop and test a fix 21 | 4. We will prepare a security advisory and release timeline 22 | 5. We will release the fix and publish the security advisory 23 | 24 | Thank you for helping keep Sparrow secure! -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add 'backend' label to any changes in src directory 2 | backend: 3 | - src/modules/**/* 4 | - src/services/**/* 5 | 6 | documentation: 7 | - docs/**/* 8 | - README.md 9 | - '**/*.md' 10 | 11 | dependencies: 12 | - package.json 13 | - pnpm-lock.yaml 14 | - yarn.lock 15 | 16 | github-actions: 17 | - .github/**/* 18 | 19 | tests: 20 | - '**/*.test.ts' 21 | - '**/*.spec.ts' 22 | - tests/**/* 23 | 24 | configuration: 25 | - '*.config.js' 26 | - '*.config.ts' 27 | - .env.example 28 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | Add your description here. 3 | 4 | ### Add Issue Number 5 | Fixes # 6 | 7 | ### Add Screenshots/GIFs 8 | If applicable, add relevant screenshot/gif here. 9 | 10 | ### Add Known Issue 11 | If applicable, add any known issues. 12 | 13 | ### Contribution Checklist: 14 | - [ ] **The pull request only addresses one issue or adds one feature.** 15 | - [ ] **I have linked an issue to the pull request.** 16 | - [ ] **I have linked a PR type label to the pull request.** 17 | - [ ] **The pull request does not introduce any breaking changes** 18 | - [ ] **I have added screenshots or gifs to help explain the change if applicable.** 19 | - [ ] **I have read the [contribution guidelines](../../docs/CONTRIBUTING.md).** 20 | 21 | Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests. -------------------------------------------------------------------------------- /.github/workflows/development.yml: -------------------------------------------------------------------------------- 1 | name: Development 2 | on: 3 | push: 4 | branches: 5 | - development 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | 13 | - uses: Azure/docker-login@v1 14 | with: 15 | login-server: sparrowprod.azurecr.io 16 | username: ${{ secrets.REGISTRY_USERNAME }} 17 | password: ${{ secrets.REGISTRY_PASSWORD }} 18 | 19 | - run: | 20 | docker build . -t sparrowprod.azurecr.io/sparrow-api:${{ github.run_number }} 21 | docker push sparrowprod.azurecr.io/sparrow-api:${{ github.run_number }} 22 | deploy: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@master 27 | - uses: richardrigutins/replace-in-files@v1 28 | with: 29 | files: "./deploymentManifests/deployment.yml" 30 | search-text: '_BUILD__ID_' 31 | replacement-text: '${{ github.run_number }}' 32 | 33 | - uses: azure/setup-kubectl@v2.0 34 | 35 | - uses: Azure/k8s-set-context@v2 36 | with: 37 | kubeconfig: ${{ secrets.KUBE_CONFIG }} 38 | 39 | - uses: Azure/k8s-deploy@v4 40 | with: 41 | action: deploy 42 | namespace: sparrow-dev 43 | manifests: | 44 | ./deploymentManifests/deployment.yml 45 | -------------------------------------------------------------------------------- /.github/workflows/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | name: "PR Labeler" 2 | on: 3 | pull_request_target: 4 | types: [opened, synchronize, reopened] 5 | 6 | jobs: 7 | triage: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/labeler@v4 11 | with: 12 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 13 | configuration-path: .github/labeler.yml 14 | sync-labels: true -------------------------------------------------------------------------------- /.github/workflows/pr-title-check.yaml: -------------------------------------------------------------------------------- 1 | name: "PR Validation" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | validate: 17 | name: Validate Pull Request 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Validate PR Title 21 | uses: amannn/action-semantic-pull-request@v5 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.PR_GITHUB_TOKEN }} 24 | with: 25 | types: | 26 | feat 27 | fix 28 | docs 29 | style 30 | refactor 31 | test 32 | build 33 | ci 34 | chore 35 | revert 36 | l10n 37 | taxonomy 38 | requireScope: false 39 | subjectPattern: ^(?![A-Z]).+$ 40 | subjectPatternError: | 41 | The subject must start with a lowercase letter. 42 | validateSingleCommit: true 43 | validateSingleCommitMatchesPrTitle: true 44 | 45 | - name: Validate PR Size 46 | uses: actions/github-script@v6 47 | with: 48 | script: | 49 | const MAX_FILES = 50; 50 | const files = await github.rest.pulls.listFiles({ 51 | owner: context.repo.owner, 52 | repo: context.repo.repo, 53 | pull_number: context.issue.number 54 | }); 55 | 56 | if (files.data.length > MAX_FILES) { 57 | core.setFailed(`PR contains ${files.data.length} files. Maximum allowed is ${MAX_FILES}.`); 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/release-v1.yml: -------------------------------------------------------------------------------- 1 | name: Release-V1 2 | on: 3 | push: 4 | branches: 5 | - release-v1 6 | 7 | jobs: 8 | release: 9 | uses: ./.github/workflows/reusable-release-deploy.yml 10 | with: 11 | version: 'v1' 12 | secrets: 13 | REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} 14 | REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} 15 | KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }} 16 | RELEASE_NAMESPACE: ${{ secrets.RELEASE_NAMESPACE }} 17 | -------------------------------------------------------------------------------- /.github/workflows/release-v2.yml: -------------------------------------------------------------------------------- 1 | name: Release-V2 2 | on: 3 | push: 4 | branches: 5 | - release-v2 6 | 7 | jobs: 8 | release: 9 | uses: ./.github/workflows/reusable-release-deploy.yml 10 | with: 11 | version: 'v2' 12 | secrets: 13 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} 14 | DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} 15 | KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }} 16 | RELEASE_NAMESPACE: ${{ secrets.RELEASE_NAMESPACE }} -------------------------------------------------------------------------------- /.github/workflows/reusable-release-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Reusable Release Deploy 2 | on: 3 | workflow_call: 4 | inputs: 5 | version: 6 | required: true 7 | type: string 8 | description: 'API version (v0, v1, v2, etc)' 9 | secrets: 10 | DOCKERHUB_USERNAME: 11 | required: true 12 | DOCKERHUB_TOKEN: 13 | required: true 14 | KUBE_CONFIG: 15 | required: true 16 | RELEASE_NAMESPACE: 17 | required: true 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@master 24 | 25 | - uses: docker/login-action@v3 26 | with: 27 | username: ${{ secrets.DOCKERHUB_USERNAME }} 28 | password: ${{ secrets.DOCKERHUB_TOKEN }} 29 | 30 | - run: | 31 | docker build . -t sparrowapi/sparrow-api:${{ github.run_number }} 32 | docker push sparrowapi/sparrow-api:${{ github.run_number }} 33 | deploy: 34 | needs: build 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@master 38 | 39 | - uses: richardrigutins/replace-in-files@v1 40 | with: 41 | files: "./deploymentManifests/release-${{ inputs.version }}.yml" 42 | search-text: '_BUILD__ID_' 43 | replacement-text: '${{ github.run_number }}' 44 | 45 | - uses: azure/setup-kubectl@v2.0 46 | 47 | - uses: Azure/k8s-set-context@v2 48 | with: 49 | kubeconfig: ${{ secrets.KUBE_CONFIG }} 50 | 51 | - uses: Azure/k8s-deploy@v4 52 | with: 53 | action: deploy 54 | namespace: ${{ secrets.RELEASE_NAMESPACE }} 55 | manifests: | 56 | ./deploymentManifests/release-${{ inputs.version }}.yml -------------------------------------------------------------------------------- /.github/workflows/sonarqube.yml: -------------------------------------------------------------------------------- 1 | name: Sonarqube 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | branches: 7 | - development 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | 16 | permissions: # permission to comment a PR 17 | contents: read 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 24 | 25 | - uses: sonarsource/sonarqube-scan-action@master 26 | env: 27 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 28 | SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} 29 | 30 | # If you wish to fail your job when the Quality Gate is red, uncomment the following lines. 31 | # Check the Quality Gate status. 32 | # - name: SonarQube Quality Gate check 33 | # id: sonarqube-quality-gate-check 34 | # uses: sonarsource/sonarqube-quality-gate-action@master 35 | # # #Enforce a timeout to fail the step after a specific time. 36 | # timeout-minutes: 5 37 | # env: 38 | # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 39 | # SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} # OPTIONAL 40 | 41 | # PR Decoration 42 | - name: SonarQube Pull Request Comment 43 | if: always() 44 | uses: campos-pay/sonarqube-pr-comment@main 45 | with: 46 | sonar_token: ${{ secrets.SONAR_TOKEN }} 47 | sonar_host_url: ${{ secrets.SONAR_HOST_URL }} 48 | sonar_projectkey: ${{ vars.SONAR_PROJECT_KEY }} #github.event.repository.name 49 | github-token: ${{ secrets.GITHUB_TOKEN }} 50 | repo_name: ${{ github.repository }} 51 | pr_number: ${{ github.event.pull_request.number }} 52 | 53 | - uses: phwt/sonarqube-quality-gate-action@v1 54 | id: quality-gate-check 55 | with: 56 | sonar-project-key: ${{ vars.SONAR_PROJECT_KEY }} 57 | sonar-host-url: ${{ secrets.SONAR_HOST_URL }} 58 | sonar-token: ${{ secrets.SONAR_TOKEN }} 59 | github-token: ${{ secrets.GITHUB_TOKEN }} #PR_GITHUB_TOKEN 60 | 61 | - name: Output result 62 | run: | 63 | echo "${{ steps.quality-gate-check.outputs.project-status }}" 64 | echo "${{ steps.quality-gate-check.outputs.quality-gate-result }}" 65 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v8 11 | with: 12 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity.' 13 | stale-pr-message: 'This PR is stale because it has been open 45 days with no activity.' 14 | close-issue-message: 'This issue was closed because it has been stale for 14 days with no activity.' 15 | close-pr-message: 'This PR was closed because it has been stale for 14 days with no activity.' 16 | days-before-issue-stale: 30 17 | days-before-pr-stale: 45 18 | days-before-issue-close: 14 19 | days-before-pr-close: 14 20 | exempt-issue-labels: 'pinned,security,bug' 21 | exempt-pr-labels: 'pinned,security,dependencies' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | lerna-debug.log 16 | npm-debug.log 17 | yarn-error.log 18 | **/npm-debug.log 19 | 20 | # environment 21 | .env 22 | 23 | # build 24 | /dist 25 | 26 | # tests 27 | /coverage 28 | /.nyc_output 29 | build/config\.gypi 30 | 31 | # IDEs and editors 32 | /.idea 33 | .project 34 | .classpath 35 | .c9/ 36 | *.launch 37 | .settings/ 38 | *.sublime-workspace 39 | 40 | # IDE - VSCode 41 | .vscode/* 42 | !.vscode/tasks.json 43 | !.vscode/launch.json 44 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if ! head -1 "$1" | grep -qE "^(feat|fix|ci|chore|docs|test|style|refactor|perf|build|revert)(\(.+?\))?: .{1,}\[.{0,10}\]$"; then 3 | echo "Aborting commit. Your commit message is invalid." >&2 4 | exit 1 5 | fi 6 | if ! head -1 "$1" | grep -qE "^.{1,88}$"; then 7 | echo "Aborting commit. Your commit message is too long." >&2 8 | exit 1 9 | fi -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint:staged:fix -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": ["prettier --write", "eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "doubleQuote": true, 3 | "trailingComma": "all", 4 | "endOfLine": "lf" 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.minimap.enabled": false, 3 | "[javascript]": { 4 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 5 | }, 6 | "svelte.enable-ts-plugin": true, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[typescriptreact]": { 11 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 12 | }, 13 | "explorer.confirmDelete": false, 14 | "[jsonc]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[css]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "editor.formatOnSave": true, 21 | "[javascriptreact]": { 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "[json]": { 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "[html]": { 28 | "editor.defaultFormatter": "esbenp.prettier-vscode" 29 | }, 30 | "[svelte]": { 31 | "editor.defaultFormatter": "svelte.svelte-vscode" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS deps 2 | WORKDIR /app 3 | 4 | # Copy only the files needed to install dependencies 5 | COPY package.json pnpm-lock.yaml* ./ 6 | 7 | # Install dependencies for native module builds 8 | RUN apk update && apk add --no-cache python3 py3-pip build-base gcc 9 | 10 | # Install dependencies with the preferred package manager 11 | RUN npm i -g pnpm@latest 12 | RUN pnpm install --frozen-lockfile 13 | # RUN corepack enable pnpm && pnpm i --frozen-lockfile 14 | 15 | FROM node:18-alpine AS builder 16 | WORKDIR /app 17 | 18 | COPY --from=deps /app/node_modules ./node_modules 19 | 20 | # Copy the rest of the files 21 | COPY . . 22 | 23 | # Run build with the preferred package manager 24 | # RUN corepack enable pnpm && pnpm build 25 | RUN npm i -g pnpm@latest 26 | RUN pnpm install --frozen-lockfile 27 | RUN pnpm build 28 | 29 | # Set NODE_ENV environment variable 30 | ENV NODE_ENV production 31 | 32 | # Re-run install only for production dependencies 33 | # RUN corepack enable pnpm && pnpm i --frozen-lockfile --prod 34 | RUN pnpm install --frozen-lockfile --prod 35 | 36 | FROM node:18-alpine AS runner 37 | WORKDIR /app 38 | 39 | # Create the logs directory and give ownership to the node user 40 | RUN mkdir -p /app/logs && chown -R node:node /app 41 | 42 | # Copy the bundled code from the builder stage 43 | COPY --from=builder --chown=node:node /app/dist ./dist 44 | COPY --from=builder --chown=node:node /app/node_modules ./node_modules 45 | COPY --from=builder --chown=node:node /app/src/modules/views ./dist/src/modules/views 46 | 47 | # Use the node user from the image 48 | USER node 49 | 50 | # Expose application port (optional, specify the port your app uses) 51 | EXPOSE 9000 9001 9002 52 | 53 | # Start the server 54 | CMD ["node", "dist/src/main.js"] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | # MongoDB 5 | mongo: 6 | image: mongo:7.0 7 | ports: 8 | - "27017:27017" 9 | environment: 10 | MONGO_INITDB_ROOT_USERNAME: sparowapp 11 | MONGO_INITDB_ROOT_PASSWORD: sparrow123 12 | networks: 13 | - localnet 14 | 15 | # Kafka 16 | kafka: 17 | image: bitnami/kafka:3.4.1 18 | hostname: kafka 19 | ports: 20 | - "9092:9092" 21 | volumes: 22 | - "kafka_data:/bitnami" 23 | environment: 24 | - KAFKA_ENABLE_KRAFT=yes 25 | - KAFKA_CFG_PROCESS_ROLES=broker,controller 26 | - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER 27 | - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094 28 | - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT 29 | - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092,EXTERNAL://kafka:9094 30 | - KAFKA_BROKER_ID=1 31 | - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@localhost:9093 32 | - ALLOW_PLAINTEXT_LISTENER=yes 33 | - KAFKA_CFG_NODE_ID=1 34 | - KAFKA_AUTO_CREATE_TOPICS_ENABLE=true 35 | - KAFKA_CFG_NUM_PARTITIONS=2 36 | - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 37 | - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 38 | - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 39 | - KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS=0 40 | - KAFKA_MESSAGE_MAX_BYTES=1000000 41 | networks: 42 | - localnet 43 | 44 | # Api 45 | api: 46 | build: 47 | context: . 48 | ports: 49 | - "9000:9000" 50 | - "9001:9001" 51 | - "9002:9002" 52 | volumes: 53 | - ./:/src 54 | networks: 55 | - localnet 56 | depends_on: 57 | - mongo 58 | - kafka 59 | env_file: ".env" 60 | 61 | volumes: 62 | kafka_data: 63 | driver: local 64 | 65 | networks: 66 | localnet: 67 | driver: bridge 68 | -------------------------------------------------------------------------------- /docs/CODEOWNERS.md: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. The below owners will be requested for 3 | # review when someone opens a pull request. 4 | * @LordNayan @gc-codes 5 | 6 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Sparrow App 2 | 3 | First off, thanks for taking the time to contribute! 🎉🎉 4 | 5 | When contributing to this repository, please first discuss the change you wish to make via issue with the maintainers of this repository before making a change. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | ## Pull Request Process 8 | 9 | 1. Please name your PR's using Conventional Commits e.g. "fix: ..." or "feat: ..." 10 | 2. Ensure any install or build dependencies are removed before the end of the layer when doing a build. Add only relevant files to commit and ignore the rest to keep the repo clean. 11 | 3. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. 12 | 4. You should request review from the maintainers once you submit the Pull Request. 13 | 14 | ## Instructions 15 | 16 | - Git Workflow (all the commands are to run in your console) 17 | 18 | #### Step 1 19 | 20 | - Fork the repository to your GitHub account 21 | 22 | ```bash 23 | # Clone the repo 24 | git clone https://github.com//sparrow-api.git 25 | 26 | # Move to the folder 27 | cd sparrow-api 28 | 29 | # Add upstream remote 30 | git remote add upstream https://github.com/sparrowapp-dev/sparrow-api.git 31 | ``` 32 | 33 | #### Step 2 34 | 35 | - [Setup project locally](https://github.com/sparrowapp-dev/sparrow-api#installation) 36 | 37 | #### Step 3 38 | 39 | - Commit your changes and push your branch to GitHub: 40 | 41 | ```bash 42 | ## Committing and pushing your work 43 | 44 | # Ensure branch 45 | git branch 46 | 47 | # Fetch and merge with upstream/main 48 | git fetch upstream 49 | git merge upstream/main 50 | 51 | # Add untracked files 52 | git add . 53 | 54 | # Commit all changes with appropriate commit message and description 55 | # Before comminting, make sure you have followed the commit message standards 56 | # use the following prefix for commit messages 57 | # wip - Work in Progress; long term work; mainstream changes; 58 | # feat - New Feature; future planned; non-mainstream changes; 59 | # bug - Bug Fixes 60 | # exp - Experimental; random experimental features; 61 | 62 | git commit -m "fix: your-commit-message" 63 | 64 | # Push changes to your forked repository 65 | git push origin //{} 66 | 67 | ## Creating the PR using GitHub Website 68 | # Create Pull Request from //{} branch in your forked repository to the master branch in the upstream repository 69 | # After creating PR, add a Reviewer (Any Admin) and yourself as the assignee 70 | # Link Pull Request to appropriate Issue, or Project+Milestone (if no issue created) 71 | # IMPORTANT: Do Not Merge the PR unless specifically asked to by an admin. 72 | ``` 73 | 74 | - After PR Merge 75 | 76 | ```bash 77 | # Delete branch from forked repo 78 | git branch -d //{} 79 | git push --delete origin //{} 80 | 81 | # Fetch and merge with upstream/main 82 | git checkout main 83 | git pull upstream 84 | git push origin 85 | ``` 86 | 87 | - Always follow [commit message standards](https://www.conventionalcommits.org/en/v1.0.0/) 88 | - About the [fork-and-branch workflow](https://blog.scottlowe.org/2015/01/27/using-fork-branch-git-workflow/) 89 | -------------------------------------------------------------------------------- /docs/ENV.md: -------------------------------------------------------------------------------- 1 | # Points to remember 2 | 3 | 1. You need to have a proper SMTP server to enble mailing capabilities in Sparrow. If you are a contributor and do no to have a SMTP server then set the below env to false 4 | 5 | `SMTP_ENABLED=false` 6 | 7 | In case you set the above env to 'false', you wont be able to get verification code to create new users, reset password or any other in-app emails. You can use the following default email/password combination to login to Sparrow: 8 | 9 | Email - dev@sparrow.com
10 | Password - 12345678@ 11 | 12 | 2. There are multiple ways to setup Kafka and Mongo. You need to change the below env variables accordingly, 13 | 14 | `DB_URL and KAFKA_BROKER` 15 | 16 | - If you used the provided scripts to run the API server and Kafka/Mongo inside Docker containers, use: 17 | 18 | `KAFKA_BROKER=kafka:9094` 19 | 20 | `DB_URL=mongodb://sparowapp:sparrow123@mongo:27017` 21 | 22 | - If Kafka/Mongo is running directly on your machine or on a different host: 23 | 24 | `KAFKA_BROKER=[HOST]:[PORT]` 25 | 26 | `DB_URL=mongodb://[USERNAME]:[PASSWORD]@[HOST]:[PORT]` -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | import { pathsToModuleNameMapper } from "ts-jest"; 3 | import { compilerOptions } from "./tsconfig.json"; 4 | 5 | const config: Config.InitialOptions = { 6 | moduleFileExtensions: ["js", "json", "ts"], 7 | modulePaths: ["."], 8 | testRegex: ".*\\.spec\\.ts$", 9 | transform: { 10 | "^.+\\.(t|j)s$": "ts-jest", 11 | }, 12 | collectCoverageFrom: ["src/**/*.(t|j)s"], 13 | coveragePathIgnorePatterns: ["src/server/console", "src/server/migration"], 14 | coverageDirectory: "coverage", 15 | testEnvironment: "node", 16 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 17 | prefix: "/", 18 | }), 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /migrations/ai-model.migration.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, OnModuleInit } from "@nestjs/common"; 2 | import { Db, ObjectId } from "mongodb"; 3 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 4 | import { ChatBotStats } from "@src/modules/common/models/chatbot-stats.model"; 5 | 6 | @Injectable() 7 | export class ChatBotStatsAiModelMigration implements OnModuleInit { 8 | private hasRun = false; 9 | constructor(@Inject("DATABASE_CONNECTION") private db: Db) {} 10 | 11 | async onModuleInit(): Promise { 12 | // Check if migration has already run 13 | if (this.hasRun) return; 14 | 15 | try { 16 | console.log( 17 | `\n\x1b[32m[Nest]\x1b[0m \x1b[32mExecuting ChatBotStats AI Model Migration...`, 18 | ); 19 | 20 | const chatbotStatsCollection = this.db.collection( 21 | Collections.CHATBOTSTATS, 22 | ); 23 | 24 | const documents = await chatbotStatsCollection 25 | .find({ 26 | tokenStats: { $exists: true }, 27 | aiModel: { $exists: false }, 28 | }) 29 | .toArray(); 30 | 31 | for (const doc of documents) { 32 | const tokenUsage = doc.tokenStats?.tokenUsage || 0; 33 | const yearMonth = doc.tokenStats?.yearMonth || null; 34 | 35 | const aiModel = { 36 | gpt: tokenUsage, 37 | deepseek: 0, 38 | yearMonth: yearMonth, 39 | }; 40 | 41 | await chatbotStatsCollection.updateOne( 42 | { _id: new ObjectId(doc._id) }, 43 | { $set: { aiModel } }, 44 | ); 45 | } 46 | 47 | console.log(`Updated ChatBotStats`); 48 | this.hasRun = true; // Set flag after successful execution 49 | } catch (error) { 50 | console.error("Error during ChatBotStats AI Model migration:", error); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /migrations/hub.migration.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, OnModuleInit } from "@nestjs/common"; 2 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 3 | import { Team } from "@src/modules/common/models/team.model"; 4 | import { Workspace } from "@src/modules/common/models/workspace.model"; 5 | import { Db, ObjectId } from "mongodb"; 6 | 7 | @Injectable() 8 | export class HubMigration implements OnModuleInit { 9 | constructor(@Inject("DATABASE_CONNECTION") private db: Db) {} 10 | 11 | async onModuleInit(): Promise { 12 | const teams = await this.db 13 | .collection(Collections.TEAM) 14 | .find({ hubUrl: { $exists: false } }) 15 | .toArray(); 16 | 17 | for (const team of teams) { 18 | // const base = this.sanitizeName(team.name); 19 | const hubUrl = await this.generateUniqueHubUrl(team.name); 20 | 21 | await this.db 22 | .collection(Collections.TEAM) 23 | .updateOne({ _id: new ObjectId(team._id) }, { $set: { hubUrl } }); 24 | 25 | // Uodate those workspaces also. 26 | await this.db 27 | .collection(Collections.WORKSPACE) 28 | .updateMany( 29 | { "team.id": team._id.toString() }, 30 | { $set: { "team.hubUrl": hubUrl } }, 31 | ); 32 | } 33 | } 34 | 35 | private sanitizeName(name: string): string { 36 | return name 37 | .trim() 38 | .toLowerCase() 39 | .replace(/[^a-z0-9]+/g, "-") 40 | .replace(/^-+|-+$/g, ""); 41 | } 42 | 43 | private async generateUniqueHubUrl(name: string): Promise { 44 | const prefix = "https://"; 45 | const suffix = ".sparrowhub.net"; 46 | // const envPath = 47 | // process.env.NODE_ENV === "production" ? "/release/v1" : "/dev"; 48 | 49 | let base = this.sanitizeName(name); 50 | if (base.length > 50) { 51 | base = base.slice(0, 50); 52 | } 53 | const baseUrl = `${prefix}${base}`; 54 | 55 | const regexPattern = `^${baseUrl}\\d*${suffix}$`; 56 | 57 | const existingTeams = await this.db 58 | .collection(Collections.TEAM) 59 | .find({ hubUrl: { $regex: regexPattern, $options: "i" } }) 60 | .project({ hubUrl: 1 }) 61 | .toArray(); 62 | 63 | const existingUrls = new Set(existingTeams.map((team) => team.hubUrl)); 64 | const finalUrl = `${baseUrl}${suffix}`; 65 | 66 | if (!existingUrls.has(finalUrl)) { 67 | return finalUrl; 68 | } 69 | 70 | let counter = 1; 71 | while (existingUrls.has(`${baseUrl}${counter}${suffix}`)) { 72 | counter++; 73 | } 74 | 75 | return `${baseUrl}${counter}${suffix}`; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /migrations/update-workspaceType.migration.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit, Inject } from "@nestjs/common"; 2 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 3 | import { Workspace, WorkspaceType } from "@src/modules/common/models/workspace.model"; 4 | import { Db } from "mongodb"; 5 | 6 | @Injectable() 7 | export class UpdateWorkspaceTypeMigration implements OnModuleInit { 8 | private hasRun = false; 9 | constructor(@Inject("DATABASE_CONNECTION") private db: Db) {} 10 | 11 | async onModuleInit(): Promise { 12 | if (this.hasRun) { 13 | // Check if migration has already run 14 | return; 15 | } 16 | try { 17 | console.log( 18 | `\n\x1b[32m[Nest]\x1b[0m \x1b[32mExecuting Workspace Type Migration...`, 19 | ); 20 | 21 | // Update workspaceType field in Workspace collection 22 | const result = await this.db 23 | .collection(Collections.WORKSPACE) 24 | .updateMany( 25 | { workspaceType: { $exists: false } }, 26 | { $set: { workspaceType: WorkspaceType.PRIVATE } }, // Use enum value 27 | ); 28 | 29 | console.log( 30 | `\x1b[32m[Nest]\x1b[0m \x1b[32m${result.modifiedCount} workspaces updated successfully.`, 31 | ); 32 | this.hasRun = true; // Set flag after successful execution 33 | } catch (error) { 34 | console.error("Error during workspace type migration:", error); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts", 6 | "ignore": [ 7 | "src/**/*.spec.ts" 8 | ], 9 | "exec": "node --inspect-brk -r ts-node/register src/main.ts" 10 | } 11 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts", 6 | "ignore": [ 7 | "src/**/*.spec.ts" 8 | ], 9 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 10 | } 11 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=sparrow-api -------------------------------------------------------------------------------- /src/migration-chatbotstats.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { Module, Provider } from "@nestjs/common"; 3 | import { MongoClient, Db } from "mongodb"; 4 | import { ConfigModule, ConfigService } from "@nestjs/config"; 5 | import configuration from "./modules/common/config/configuration"; 6 | import { ChatBotStatsAiModelMigration } from "migrations/ai-model.migration"; 7 | 8 | const databaseProvider: Provider = { 9 | provide: "DATABASE_CONNECTION", 10 | useFactory: async (configService: ConfigService): Promise => { 11 | const dbUrl = configService.get("db.url"); 12 | 13 | if (!dbUrl) { 14 | throw new Error("Database URL is not defined in the configuration."); 15 | } 16 | 17 | const client = new MongoClient(dbUrl); 18 | await client.connect(); 19 | return client.db("sparrow"); 20 | }, 21 | inject: [ConfigService], 22 | }; 23 | 24 | @Module({ 25 | imports: [ 26 | ConfigModule.forRoot({ 27 | isGlobal: true, 28 | load: [configuration], 29 | }), 30 | ], 31 | providers: [databaseProvider, ChatBotStatsAiModelMigration], 32 | }) 33 | class MigrationModule {} 34 | 35 | async function run() { 36 | const app = await NestFactory.createApplicationContext(MigrationModule); 37 | const migration = app.get(ChatBotStatsAiModelMigration); 38 | await migration.onModuleInit(); 39 | await app.close(); 40 | } 41 | 42 | run(); 43 | -------------------------------------------------------------------------------- /src/migration.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { Module, Provider } from "@nestjs/common"; 3 | import { MongoClient, Db } from "mongodb"; 4 | import { ConfigModule, ConfigService } from "@nestjs/config"; 5 | import configuration from "./modules/common/config/configuration"; 6 | import { HubMigration } from "migrations/hub.migration"; 7 | 8 | const databaseProvider: Provider = { 9 | provide: "DATABASE_CONNECTION", 10 | useFactory: async (configService: ConfigService): Promise => { 11 | const dbUrl = configService.get("db.url"); 12 | 13 | if (!dbUrl) { 14 | throw new Error("Database URL is not defined in the configuration."); 15 | } 16 | 17 | const client = new MongoClient(dbUrl); 18 | await client.connect(); 19 | return client.db("sparrow"); 20 | }, 21 | inject: [ConfigService], 22 | }; 23 | 24 | @Module({ 25 | imports: [ 26 | ConfigModule.forRoot({ 27 | isGlobal: true, 28 | load: [configuration], 29 | }), 30 | ], 31 | providers: [databaseProvider, HubMigration], 32 | }) 33 | class MigrationModule {} 34 | 35 | async function run() { 36 | const app = await NestFactory.createApplicationContext(MigrationModule); 37 | const migration = app.get(HubMigration); 38 | await migration.onModuleInit(); 39 | await app.close(); 40 | } 41 | 42 | run(); 43 | -------------------------------------------------------------------------------- /src/migrationTestflow.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { Module, Provider } from "@nestjs/common"; 3 | import { MongoClient, Db } from "mongodb"; 4 | import { ConfigModule, ConfigService } from "@nestjs/config"; 5 | import configuration from "./modules/common/config/configuration"; 6 | import { UpdateTestFlowModelMigration } from "migrations/update-test-flow-model.migration"; 7 | 8 | const databaseProvider: Provider = { 9 | provide: "DATABASE_CONNECTION", 10 | useFactory: async (configService: ConfigService): Promise => { 11 | const dbUrl = configService.get("db.url"); 12 | 13 | if (!dbUrl) { 14 | throw new Error("Database URL is not defined in the configuration."); 15 | } 16 | 17 | const client = new MongoClient(dbUrl); 18 | await client.connect(); 19 | return client.db("sparrow"); 20 | }, 21 | inject: [ConfigService], 22 | }; 23 | 24 | @Module({ 25 | imports: [ 26 | ConfigModule.forRoot({ 27 | isGlobal: true, 28 | load: [configuration], 29 | }), 30 | ], 31 | providers: [databaseProvider, UpdateTestFlowModelMigration], 32 | }) 33 | class MigrationModule {} 34 | 35 | async function run() { 36 | const app = await NestFactory.createApplicationContext(MigrationModule); 37 | const migration = app.get(UpdateTestFlowModelMigration); 38 | await migration.onModuleInit(); 39 | await app.close(); 40 | } 41 | 42 | run(); 43 | -------------------------------------------------------------------------------- /src/migrationWorkspaceType.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { Module, Provider } from "@nestjs/common"; 3 | import { MongoClient, Db } from "mongodb"; 4 | import { ConfigModule, ConfigService } from "@nestjs/config"; 5 | import configuration from "./modules/common/config/configuration"; 6 | import { UpdateWorkspaceTypeMigration } from "migrations/update-workspaceType.migration"; 7 | 8 | const databaseProvider: Provider = { 9 | provide: "DATABASE_CONNECTION", 10 | useFactory: async (configService: ConfigService): Promise => { 11 | const dbUrl = configService.get("db.url"); 12 | 13 | if (!dbUrl) { 14 | throw new Error("Database URL is not defined in the configuration."); 15 | } 16 | 17 | const client = new MongoClient(dbUrl); 18 | await client.connect(); 19 | return client.db("sparrow"); 20 | }, 21 | inject: [ConfigService], 22 | }; 23 | 24 | @Module({ 25 | imports: [ 26 | ConfigModule.forRoot({ 27 | isGlobal: true, 28 | load: [configuration], 29 | }), 30 | ], 31 | providers: [databaseProvider, UpdateWorkspaceTypeMigration], 32 | }) 33 | class MigrationModule {} 34 | 35 | async function run() { 36 | const app = await NestFactory.createApplicationContext(MigrationModule); 37 | const migration = app.get(UpdateWorkspaceTypeMigration); 38 | await migration.onModuleInit(); 39 | await app.close(); 40 | } 41 | 42 | run(); 43 | -------------------------------------------------------------------------------- /src/modules/app/app.repository.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "@nestjs/common"; 2 | import { Db } from "mongodb"; 3 | 4 | /** 5 | * App Repository 6 | */ 7 | @Injectable() 8 | export class AppRepository { 9 | /** 10 | * Constructor for App Repository. 11 | * @param db The MongoDB database connection injected by the NestJS dependency injection system. 12 | */ 13 | constructor(@Inject("DATABASE_CONNECTION") private db: Db) {} 14 | 15 | /** 16 | * Check if the existing database connection is active. 17 | * @returns True if the database is connected, otherwise false. 18 | */ 19 | async isMongoConnected(): Promise { 20 | try { 21 | // Perform a lightweight operation to verify connection health 22 | await this.db.admin().ping(); 23 | return true; 24 | } catch (error) { 25 | console.error("MongoDB connection lost:", error); 26 | return false; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/app/app.roles.ts: -------------------------------------------------------------------------------- 1 | import { RolesBuilder } from "nest-access-control"; 2 | 3 | export enum AppRoles { 4 | ADMIN = "ADMIN", 5 | MEMBER = "MEMBER", 6 | } 7 | 8 | /** 9 | * Roles Builder 10 | */ 11 | export const roles: RolesBuilder = new RolesBuilder(); 12 | 13 | // The default app role doesn't have readAny(users) because the user returned provides a password. 14 | // To mutate the return body of mongo queries try editing the userService 15 | roles 16 | .grant(AppRoles.ADMIN) 17 | .readOwn("user") 18 | .updateOwn("user") 19 | .deleteOwn("user") 20 | .grant(AppRoles.ADMIN) 21 | .readAny("user") 22 | .updateAny("user") 23 | .deleteAny("user"); 24 | -------------------------------------------------------------------------------- /src/modules/app/payloads/curl.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { IsNotEmpty, IsString } from "class-validator"; 3 | 4 | export class curlDto { 5 | @ApiProperty({ 6 | example: "curl google.com", 7 | }) 8 | @IsString() 9 | @IsNotEmpty() 10 | curl: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/app/payloads/subscribe.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { IsNotEmpty, IsString } from "class-validator"; 3 | 4 | export class subscribePayload { 5 | @ApiProperty({ 6 | example: "demo@gmail.com", 7 | }) 8 | @IsString() 9 | @IsNotEmpty() 10 | email: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/app/payloads/updaterJson.payload.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatusCode } from "@src/modules/common/enum/httpStatusCode.enum"; 2 | 3 | export interface UpdatorJsonRequestPayload {} 4 | 5 | export interface UpdaterJsonResponsePayload { 6 | statusCode: HttpStatusCode; 7 | data: { 8 | version: string; 9 | platforms: Platforms; 10 | }; 11 | } 12 | 13 | interface Platforms { 14 | "windows-x86_64": PlatformData; 15 | "darwin-aarch64": PlatformData; 16 | "darwin-x86_64": PlatformData; 17 | } 18 | 19 | interface PlatformData { 20 | signature: string; 21 | url: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/common/config/env.validation.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "class-transformer"; 2 | import { IsEnum, IsNotEmpty, IsNumber, IsString } from "class-validator"; 3 | 4 | export enum Env { 5 | DEV = "DEV", 6 | PROD = "PROD", 7 | } 8 | 9 | export class EnvironmentVariables { 10 | @Type(() => Number) 11 | @IsNumber() 12 | @IsNotEmpty() 13 | PORT: number; 14 | 15 | @IsString() 16 | @IsNotEmpty() 17 | @IsEnum(Env) 18 | APP_ENV: number; 19 | 20 | @IsString() 21 | @IsNotEmpty() 22 | APP_URL: number; 23 | 24 | @IsString() 25 | @IsNotEmpty() 26 | JWT_SECRET_KEY: number; 27 | 28 | @Type(() => Number) 29 | @IsNumber() 30 | JWT_EXPIRATION_TIME: number; 31 | 32 | @IsString() 33 | @IsNotEmpty() 34 | DB_URL: string; 35 | 36 | @IsString() 37 | @IsNotEmpty() 38 | REFRESH_TOKEN_SECRET_KEY: string; 39 | 40 | @Type(() => Number) 41 | @IsNumber() 42 | REFRESH_TOKEN_EXPIRATION_TIME: number; 43 | 44 | @Type(() => Number) 45 | @IsNumber() 46 | REFRESH_TOKEN_MAX_LIMIT: number; 47 | 48 | @IsString() 49 | @IsNotEmpty() 50 | KAFKA_BROKER: string; 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/common/decorators/roles.decorators.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from "@nestjs/common"; 2 | 3 | export const ROLES_KEY = "roles"; 4 | export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); 5 | -------------------------------------------------------------------------------- /src/modules/common/enum/ai-services.enum.ts: -------------------------------------------------------------------------------- 1 | export enum AiService { 2 | SparrowAI = "sparrow-ai", 3 | LlmEvaluation = "llm-evaluation" 4 | } 5 | 6 | export enum Models { 7 | Anthropic = "anthropic", 8 | Google = "google", 9 | OpenAI = "openai", 10 | DeepSeek = "deepseek", 11 | GPT = "gpt" // For AzureOpenai 12 | } 13 | 14 | export enum ClaudeModelVersion { 15 | Claude_3_Opus = "claude-3-opus-20240229", 16 | Claude_3_Sonnet = "claude-3-5-sonnet-20240620", // Claude Sonnet 3 is Claude 3.5 Sonnet (Old) 17 | Claude_3_Haiku = "claude-3-haiku-20240307", 18 | Claude_3_5_Sonnet = "claude-3-5-sonnet-20241022", 19 | Claude_3_5_Haiku = "claude-3-5-haiku-20241022" 20 | } 21 | 22 | export enum GoogleModelVersion { 23 | Gemini_1_5_Flash = "gemini-1.5-flash", 24 | Gemini_1_5_Flash_8b= "gemini-1.5-flash-8b", 25 | Gemini_1_5_Pro = "gemini-1.5-pro", 26 | Gemini_2_0_Flash = "gemini-2.0-flash" 27 | } 28 | 29 | export enum OpenAIModelVersion { 30 | GPT_4o = "gpt-4o", 31 | GPT_4o_Mini = "gpt-4o-mini", 32 | GPT_4_5_Preview = "gpt-4.5-preview", 33 | GPT_4_Turbo = "gpt-4-turbo", 34 | GPT_4 = "gpt-4", 35 | GPT_4_1 = "gpt-4.1", 36 | GPT_o1 = "o1", 37 | GPT_o1_Mini = "o1-mini", 38 | GPT_o3_Mini = "o3-mini", 39 | GPT_3_5_Turbo = "gpt-3.5-turbo" 40 | } 41 | 42 | export enum DeepSeepModelVersion { 43 | DeepSeek_R1 = "deepseek-reasoner", 44 | DeepSeek_V3 = "deepseek-chat" 45 | } 46 | 47 | export enum Roles { 48 | system = "system", 49 | user = "user", 50 | assistant = "assistant", 51 | model = "model" 52 | } -------------------------------------------------------------------------------- /src/modules/common/enum/database.collection.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Collections { 2 | WORKSPACE = "workspace", 3 | USER = "user", 4 | TEAM = "team", 5 | COLLECTION = "collection", 6 | EARLYACCESS = "earlyaccess", 7 | ENVIRONMENT = "environment", 8 | FEATURES = "features", 9 | BRANCHES = "branches", 10 | FEEDBACK = "feedback", 11 | UPDATES = "updates", 12 | CHATBOTSTATS = "chatbotstats", 13 | TESTFLOW = "testflow", 14 | USERINVITES = "userinvites", 15 | AILOGS = "ailogs", 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/common/enum/error-messages.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorMessages { 2 | ExpiredToken = "Token has expired", 3 | Unauthorized = "Unauthorized Access", 4 | SystemUnauthorized = "UnauthorizedException", 5 | TokenExpiredError = "TokenExpiredError", 6 | VerificationCodeExpired = "Verification Code Expired", 7 | BadRequestError = "Bad Request", 8 | PasswordExist = "Old Password and New Password cannot be same", 9 | InvalidFile = "Invalid File Type", 10 | JWTFailed = "JWT Failed", 11 | MagicCodeExpired = "Magic Code Expired", 12 | } 13 | 14 | export enum FeedbackErrorMessages { 15 | VideoCountLimit = "Only one video per feedback is allowed", 16 | VideoSizeLimit = "Video size should be less than 10 MB", 17 | FilesCountLimit = "Files Count should not be greater than 5", 18 | ImageSizeLimit = "Image size should be less than 2 MB", 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/common/enum/feedback.enum.ts: -------------------------------------------------------------------------------- 1 | export enum FeedbackType { 2 | BUG = "Bug", 3 | FEEDBACK = "Feedback", 4 | FEATURE = "Feature Request", 5 | } 6 | 7 | export enum FeebackSubCategory { 8 | USABILITY = "Usability", 9 | DOCUMENTATION = "Documentation", 10 | PERFORMANCE = "Performance", 11 | LOW = "Low", 12 | MEDIUM = "Medium", 13 | HIGH = "High", 14 | CRITICAL = "Critical", 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/common/enum/roles.enum.ts: -------------------------------------------------------------------------------- 1 | export enum WorkspaceRole { 2 | ADMIN = "admin", 3 | EDITOR = "editor", 4 | VIEWER = "viewer", 5 | } 6 | 7 | export enum TeamRole { 8 | OWNER = "owner", 9 | ADMIN = "admin", 10 | MEMBER = "member", 11 | } 12 | 13 | export enum Role { 14 | ADMIN = "admin", 15 | WRITER = "writer", 16 | READER = "reader", 17 | } 18 | 19 | export enum Permission { 20 | CreateTeam = "createTeam", 21 | UpdateTeam = "updateTeam", 22 | DeleteTeam = "deleteTeam", 23 | // Define other permissions 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/common/enum/subscription.enum.ts: -------------------------------------------------------------------------------- 1 | export enum SUBSCRIPTION { 2 | USER_ADDED_TO_TEAM_SUBSCRIPTION = "USER_ADDED_TO_TEAM_SUBSCRIPTION", 3 | USER_REMOVED_FROM_TEAM_SUBSCRIPTION = "USER_REMOVED_FROM_TEAM_SUBSCRIPTION", 4 | CREATE_USER_SUBSCRIPTION = "CREATE_USER_SUBSCRIPTION", 5 | TEAM_ADMIN_ADDED_SUBSCRIPTION = "TEAM_ADMIN_ADDED_SUBSCRIPTION", 6 | TEAM_ADMIN_DEMOTED_SUBSCRIPTION = "TEAM_ADMIN_DEMOTED_SUBSCRIPTION", 7 | UPDATES_ADDED_SUBSCRIPTION = "UPDATES_ADDED_SUBSCRIPTION", 8 | AI_RESPONSE_GENERATED_SUBSCRIPTION = "AI_RESPONSE_GENERATED_SUBSCRIPTION", 9 | TEAM_DETAILS_UPDATED_SUBSCRIPTION = "TEAM_DETAILS_UPDATED_SUBSCRIPTION", 10 | AI_LOGS_GENERATOR_SUBSCRIPTION = "AI_LOGS_GENERATOR_SUBSCRIPTION", 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/common/enum/topic.enum.ts: -------------------------------------------------------------------------------- 1 | export enum TOPIC { 2 | USER_ADDED_TO_TEAM_TOPIC = "user_added_to_team_topic", 3 | USER_REMOVED_FROM_TEAM_TOPIC = "user_removed_from_team_topic", 4 | CREATE_USER_TOPIC = "create_user_topic", 5 | TEAM_ADMIN_ADDED_TOPIC = "team_admin_added_topic", 6 | TEAM_ADMIN_DEMOTED_TOPIC = "team_admin_demoted_topic", 7 | UPDATES_ADDED_TOPIC = "updates_added_topic", 8 | AI_RESPONSE_GENERATED_TOPIC = "ai_response_generated_topic", 9 | TEAM_DETAILS_UPDATED_TOPIC = "team_details_updated_topic", 10 | AI_ACTIVITY_LOG_TOPIC = "ai_activity_log_topic", 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/common/enum/updates.enum.ts: -------------------------------------------------------------------------------- 1 | export enum UpdatesType { 2 | WORKSPACE = "workspace", 3 | COLLECTION = "collection", 4 | FOLDER = "folder", 5 | REQUEST = "request", 6 | ROLE = "role", 7 | ENVIRONMENT = "environment", 8 | TESTFLOW = "testflow", 9 | WEBSOCKET = "websocket", 10 | SOCKETIO = "socketio", 11 | GRAPHQL = "graphql", 12 | REQUEST_RESPONSE = "request_response", 13 | MOCK_REQUEST = "mock_request", 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/common/exception/logging.exception-filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | HttpException, 6 | Inject, 7 | BadRequestException, 8 | } from "@nestjs/common"; 9 | import { FastifyReply } from "fastify"; 10 | import { PinoLogger } from "nestjs-pino"; 11 | import { InsightsService } from "../services/insights.service"; 12 | import * as Sentry from "@sentry/nestjs"; 13 | @Catch() 14 | export class LoggingExceptionsFilter implements ExceptionFilter { 15 | constructor( 16 | @Inject("ErrorLogger") private readonly errorLogger: PinoLogger, 17 | private readonly insightsService: InsightsService, 18 | ) {} 19 | 20 | catch(exception: HttpException, host: ArgumentsHost) { 21 | if (exception instanceof HttpException) { 22 | const ctx = host.switchToHttp(); 23 | const response = ctx.getResponse(); 24 | const status = exception.getStatus(); 25 | this.errorLogger.error(exception); 26 | const req = ctx.getRequest(); 27 | // Create a standard Error object 28 | const error = new Error(exception.message); 29 | error.name = exception.name; 30 | error.stack = exception.stack; 31 | // Log exception to Application Insights 32 | const client = this.insightsService.getClient(); 33 | if (client) { 34 | try { 35 | client?.trackException({ 36 | exception: error, 37 | properties: { 38 | method: req.method, 39 | url: req.url, 40 | status, 41 | }, 42 | }); 43 | } catch (e) { 44 | console.error(e); 45 | } 46 | } else { 47 | console.error("Application Insights client is not initialized."); 48 | } 49 | return response.status(status).send({ 50 | statusCode: status, 51 | message: exception.message, 52 | error: exception.name, 53 | }); 54 | } else { 55 | Sentry.captureException(exception); 56 | throw new BadRequestException(exception); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/modules/common/guards/google-oauth.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from "@nestjs/common"; 2 | import { AuthGuard } from "@nestjs/passport"; 3 | 4 | @Injectable() 5 | export class GoogleOAuthGuard extends AuthGuard("google") { 6 | handleRequest(err: string, user: any, info: any, context: ExecutionContext) { 7 | const request = context.switchToHttp().getRequest(); 8 | const error = request.query.error; 9 | if (error) { 10 | return error; 11 | } 12 | return user; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/common/guards/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExecutionContext, 3 | Injectable, 4 | UnauthorizedException, 5 | } from "@nestjs/common"; 6 | import { AuthGuard } from "@nestjs/passport"; 7 | import { ErrorMessages } from "../enum/error-messages.enum"; 8 | 9 | @Injectable() 10 | export class JwtAuthGuard extends AuthGuard("jwt") { 11 | canActivate(context: ExecutionContext) { 12 | return super.canActivate(context); 13 | } 14 | 15 | handleRequest(err: any, user: any, info: any) { 16 | if (err || !user) { 17 | if (info.name === ErrorMessages.TokenExpiredError) { 18 | throw new UnauthorizedException(ErrorMessages.ExpiredToken); 19 | } 20 | throw new UnauthorizedException(ErrorMessages.JWTFailed); 21 | } 22 | 23 | return user; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/common/guards/refresh-token.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExecutionContext, 3 | Injectable, 4 | UnauthorizedException, 5 | } from "@nestjs/common"; 6 | import { AuthGuard } from "@nestjs/passport"; 7 | import { ErrorMessages } from "../enum/error-messages.enum"; 8 | 9 | @Injectable() 10 | export class RefreshTokenGuard extends AuthGuard("jwt-refresh") { 11 | canActivate(context: ExecutionContext) { 12 | return super.canActivate(context); 13 | } 14 | 15 | handleRequest(err: any, user: any) { 16 | if (err || !user) { 17 | throw new UnauthorizedException(ErrorMessages.JWTFailed); 18 | } 19 | return user; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/common/guards/roles.guard.ts: -------------------------------------------------------------------------------- 1 | // src/modules/common/guards/roles.guard.ts 2 | import { 3 | Injectable, 4 | CanActivate, 5 | ExecutionContext, 6 | ForbiddenException, 7 | } from "@nestjs/common"; 8 | import { Reflector } from "@nestjs/core"; 9 | import { ROLES_KEY } from "../decorators/roles.decorators"; 10 | 11 | @Injectable() 12 | export class RolesGuard implements CanActivate { 13 | constructor(private reflector: Reflector) {} 14 | 15 | canActivate(context: ExecutionContext): boolean { 16 | const requiredRoles = this.reflector.getAllAndMerge(ROLES_KEY, [ 17 | context.getHandler(), 18 | context.getClass(), 19 | ]); 20 | 21 | if (!requiredRoles || requiredRoles.length === 0) return true; 22 | 23 | const { user } = context.switchToHttp().getRequest(); 24 | 25 | if (!user || !requiredRoles.includes(user.role)) { 26 | throw new ForbiddenException( 27 | `Access denied. Required role(s): ${requiredRoles.join(", ")}`, 28 | ); 29 | } 30 | 31 | return true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/common/models/ai-log.model.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "class-transformer"; 2 | import { 3 | IsArray, 4 | IsBoolean, 5 | IsDate, 6 | IsMongoId, 7 | IsNotEmpty, 8 | IsNumber, 9 | IsOptional, 10 | IsString, 11 | ValidateNested, 12 | } from "class-validator"; 13 | import { ApiProperty } from "@nestjs/swagger"; 14 | 15 | /** 16 | * AiLogs class is used to store statistics and feedback related to chatbot interactions. 17 | */ 18 | export class AiLogs { 19 | 20 | /** 21 | * The unique identifier of the user who interacted with the chatbot. 22 | * This field is required and must be a valid MongoDB ObjectId. 23 | */ 24 | @ApiProperty() 25 | @IsMongoId() 26 | @IsNotEmpty() 27 | @IsString() 28 | userId: string; 29 | 30 | 31 | @ApiProperty() 32 | @IsMongoId() 33 | @IsNotEmpty() 34 | @IsString() 35 | activity: string; 36 | 37 | 38 | @ApiProperty() 39 | @IsMongoId() 40 | @IsNotEmpty() 41 | @IsString() 42 | model: string; 43 | 44 | /** 45 | * The total number of tokens consumed during the chatbot interaction. 46 | * This field is required. 47 | */ 48 | @ApiProperty() 49 | @IsNumber() 50 | @IsNotEmpty() 51 | tokenConsumed: number; 52 | 53 | @ApiProperty() 54 | @IsMongoId() 55 | @IsNotEmpty() 56 | @IsString() 57 | thread_id: string; 58 | 59 | /** 60 | * The date and time when the chatbot statistics were created. 61 | * This field is optional. 62 | */ 63 | @IsDate() 64 | @IsOptional() 65 | createdAt?: Date; 66 | 67 | /** 68 | * The identifier of the user who created the chatbot statistics. 69 | * This field is optional. 70 | */ 71 | @IsString() 72 | @IsOptional() 73 | createdBy?: string; 74 | 75 | } -------------------------------------------------------------------------------- /src/modules/common/models/branch.model.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "class-transformer"; 2 | import { 3 | IsArray, 4 | IsDate, 5 | IsMongoId, 6 | IsNotEmpty, 7 | IsOptional, 8 | IsString, 9 | ValidateNested, 10 | } from "class-validator"; 11 | import { ApiProperty } from "@nestjs/swagger"; 12 | import { CollectionItem } from "./collection.model"; 13 | import { ObjectId } from "mongodb"; 14 | 15 | export class Branch { 16 | @ApiProperty() 17 | @IsString() 18 | @IsNotEmpty() 19 | name: string; 20 | 21 | @ApiProperty() 22 | @IsMongoId() 23 | @IsNotEmpty() 24 | collectionId: ObjectId; 25 | 26 | @ApiProperty({ type: [CollectionItem] }) 27 | @IsArray() 28 | @ValidateNested({ each: true }) 29 | @Type(() => CollectionItem) 30 | items: CollectionItem[]; 31 | 32 | @IsDate() 33 | @IsOptional() 34 | createdAt?: Date; 35 | 36 | @IsDate() 37 | @IsOptional() 38 | updatedAt?: Date; 39 | 40 | @IsString() 41 | @IsOptional() 42 | createdBy?: string; 43 | 44 | @IsString() 45 | @IsOptional() 46 | updatedBy?: string; 47 | } 48 | -------------------------------------------------------------------------------- /src/modules/common/models/collection.rxdb.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthModeEnum, 3 | BodyModeEnum, 4 | ItemTypeEnum, 5 | SourceTypeEnum, 6 | PostmanBodyModeEnum, 7 | } from "./collection.model"; 8 | 9 | export enum AddTo { 10 | Header = "Header", 11 | QueryParameter = "Query Parameter", 12 | } 13 | 14 | export class TransformedRequest { 15 | id?: string; 16 | tag?: string; 17 | operationId?: string; 18 | source: SourceTypeEnum; 19 | isDeleted?: boolean; 20 | name: string; 21 | description?: string; 22 | type: ItemTypeEnum; 23 | request: SparrowRequest; 24 | createdAt: Date; 25 | updatedAt: Date; 26 | createdBy: string; 27 | updatedBy: string; 28 | items?: TransformedRequest[]; 29 | } 30 | 31 | export interface SparrowRequest { 32 | selectedRequestBodyType?: BodyModeEnum | PostmanBodyModeEnum; 33 | selectedRequestAuthType?: AuthModeEnum; 34 | method: string; 35 | url: string; 36 | body: SparrowRequestBody; 37 | headers?: KeyValue[]; 38 | queryParams?: KeyValue[]; 39 | auth?: Auth; 40 | } 41 | 42 | // Define the RequestBody type 43 | export class SparrowRequestBody { 44 | raw?: string; 45 | urlencoded?: KeyValue[]; 46 | formdata?: FormData; 47 | } 48 | 49 | interface FormData { 50 | text: KeyValue[]; 51 | file: FormDataFileEntry[]; 52 | } 53 | 54 | export class KeyValue { 55 | key: string; 56 | value: string | unknown; 57 | checked: boolean; 58 | } 59 | 60 | interface FormDataFileEntry { 61 | key: string; 62 | value: string | unknown; 63 | checked: boolean; 64 | base: string; 65 | } 66 | 67 | export class Auth { 68 | bearerToken?: string; 69 | basicAuth?: { 70 | username: string; 71 | password: string; 72 | }; 73 | apiKey?: { 74 | authKey: string; 75 | authValue: string | unknown; 76 | addTo: AddTo; 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/modules/common/models/environment.model.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "class-transformer"; 2 | import { 3 | IsArray, 4 | IsBoolean, 5 | IsDate, 6 | IsEnum, 7 | IsMongoId, 8 | IsNotEmpty, 9 | IsOptional, 10 | IsString, 11 | ValidateNested, 12 | } from "class-validator"; 13 | import { ObjectId } from "mongodb"; 14 | 15 | export enum DefaultEnvironment { 16 | GLOBAL = "Global Variables", 17 | } 18 | 19 | export enum EnvironmentType { 20 | GLOBAL = "GLOBAL", 21 | LOCAL = "LOCAL", 22 | } 23 | 24 | export class VariableDto { 25 | @IsString() 26 | key: string; 27 | 28 | @IsString() 29 | value: string; 30 | 31 | @IsBoolean() 32 | @IsNotEmpty() 33 | @IsOptional() 34 | checked?: boolean; 35 | } 36 | 37 | export class Environment { 38 | @IsString() 39 | @IsNotEmpty() 40 | name: string; 41 | 42 | @IsArray() 43 | @Type(() => VariableDto) 44 | @ValidateNested({ each: true }) 45 | @IsOptional() 46 | variable: VariableDto[]; 47 | 48 | @IsEnum(EnvironmentType) 49 | @IsNotEmpty() 50 | type: EnvironmentType; 51 | 52 | @IsDate() 53 | @IsOptional() 54 | createdAt?: Date; 55 | 56 | @IsDate() 57 | @IsOptional() 58 | updatedAt?: Date; 59 | 60 | @IsString() 61 | @IsOptional() 62 | createdBy?: string; 63 | 64 | @IsString() 65 | @IsOptional() 66 | updatedBy?: string; 67 | } 68 | 69 | export class EnvironmentDto { 70 | @IsMongoId() 71 | @IsNotEmpty() 72 | id: ObjectId; 73 | 74 | @IsString() 75 | @IsNotEmpty() 76 | @IsOptional() 77 | name?: string; 78 | 79 | @IsEnum(EnvironmentType) 80 | @IsNotEmpty() 81 | @IsOptional() 82 | type?: EnvironmentType; 83 | } 84 | -------------------------------------------------------------------------------- /src/modules/common/models/feature.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsBoolean, 3 | IsDate, 4 | IsNotEmpty, 5 | IsOptional, 6 | IsString, 7 | } from "class-validator"; 8 | 9 | export class Feature { 10 | @IsString() 11 | @IsNotEmpty() 12 | name: string; 13 | 14 | @IsBoolean() 15 | @IsNotEmpty() 16 | isEnabled: boolean; 17 | 18 | @IsDate() 19 | @IsOptional() 20 | createdAt?: Date; 21 | 22 | @IsDate() 23 | @IsOptional() 24 | updatedAt?: Date; 25 | 26 | @IsString() 27 | @IsOptional() 28 | createdBy?: string; 29 | 30 | @IsString() 31 | @IsOptional() 32 | updatedBy?: string; 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/common/models/feedback.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsArray, 3 | IsDate, 4 | IsEnum, 5 | IsMongoId, 6 | IsNotEmpty, 7 | IsOptional, 8 | IsString, 9 | ValidateNested, 10 | } from "class-validator"; 11 | import { ApiProperty } from "@nestjs/swagger"; 12 | import { Type } from "class-transformer"; 13 | import { FeebackSubCategory, FeedbackType } from "../enum/feedback.enum"; 14 | 15 | /** 16 | * Feedback Files Model which contains uploaded file details 17 | */ 18 | export class FeedbackFiles { 19 | /** 20 | * The ID of the file. 21 | */ 22 | @ApiProperty() 23 | @IsString() 24 | @IsNotEmpty() 25 | fileId: string; 26 | 27 | /** 28 | * The name of the file. 29 | */ 30 | @ApiProperty() 31 | @IsString() 32 | @IsNotEmpty() 33 | fileName: string; 34 | 35 | /** 36 | * The URL of the file. 37 | */ 38 | @ApiProperty() 39 | @IsString() 40 | @IsNotEmpty() 41 | fileUrl: string; 42 | 43 | /** 44 | * The MIME type of the file. 45 | */ 46 | @ApiProperty() 47 | @IsString() 48 | @IsNotEmpty() 49 | mimetype: string; 50 | } 51 | 52 | /** 53 | * Feedback Model 54 | */ 55 | export class Feedback { 56 | /** 57 | * The type of feedback (e.g., Bug, Feedback). 58 | */ 59 | @ApiProperty() 60 | @IsString() 61 | @IsNotEmpty() 62 | @IsEnum(FeedbackType) 63 | type: string; 64 | 65 | /** 66 | * The sub-category of the feedback (e.g., Performance, Documentation). 67 | */ 68 | @ApiProperty() 69 | @IsString() 70 | @IsOptional() 71 | @IsEnum(FeebackSubCategory) 72 | subCategory?: string; 73 | 74 | /** 75 | * The subject of the feedback (optional). 76 | */ 77 | @ApiProperty() 78 | @IsString() 79 | @IsOptional() 80 | subject?: string; 81 | 82 | /** 83 | * The description of the feedback (optional). 84 | */ 85 | @ApiProperty() 86 | @IsString() 87 | @IsOptional() 88 | description?: string; 89 | 90 | /** 91 | * Array of files associated with the feedback (optional). 92 | */ 93 | @IsArray() 94 | @Type(() => FeedbackFiles) 95 | @ValidateNested({ each: true }) 96 | @IsOptional() 97 | files?: FeedbackFiles[]; 98 | 99 | /** 100 | * The creation date of the feedback (optional). 101 | */ 102 | @IsDate() 103 | @IsOptional() 104 | createdAt?: Date; 105 | 106 | /** 107 | * The ID of the user who created the feedback (optional). 108 | */ 109 | @IsString() 110 | @IsOptional() 111 | @IsMongoId() 112 | createdBy?: string; 113 | } 114 | -------------------------------------------------------------------------------- /src/modules/common/models/updates.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsDate, 3 | IsEnum, 4 | IsMongoId, 5 | IsNotEmpty, 6 | IsOptional, 7 | IsString, 8 | } from "class-validator"; 9 | import { UpdatesType } from "../enum/updates.enum"; 10 | 11 | /** 12 | * The Updates class represents an update entity with various properties 13 | * validated using class-validator decorators. 14 | */ 15 | export class Updates { 16 | /** 17 | * The type of update, like it's belong to workspace, environment, folder etc. 18 | */ 19 | @IsString() 20 | @IsEnum(UpdatesType) 21 | @IsNotEmpty() 22 | type: UpdatesType; 23 | 24 | /** 25 | * The message of the update. Must be a non-empty string. 26 | */ 27 | @IsString() 28 | @IsNotEmpty() 29 | message: string; 30 | 31 | /** 32 | * The ID of the workspace whose update is this. Must be a valid MongoDB ObjectId and non-empty. 33 | */ 34 | @IsMongoId() 35 | @IsNotEmpty() 36 | workspaceId: string; 37 | 38 | /** 39 | * The date when the update was created. Optional field. 40 | */ 41 | @IsDate() 42 | @IsOptional() 43 | createdAt?: Date; 44 | 45 | /** 46 | * The ID of the user who created the update. Optional field. 47 | */ 48 | @IsString() 49 | @IsOptional() 50 | createdBy?: string; 51 | 52 | /** 53 | * The ID of the user who last updated the details of the update. Optional field. 54 | */ 55 | @IsString() 56 | @IsOptional() 57 | detailsUpdatedBy?: string; 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/common/models/user-invites.model.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString, IsArray, IsDateString } from "class-validator"; 2 | 3 | export class UserInvites { 4 | @IsString() 5 | @IsNotEmpty() 6 | email: string; 7 | 8 | @IsArray() 9 | @IsString({ each: true }) 10 | teamIds: string[]; 11 | 12 | @IsDateString() 13 | createdAt: Date; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/common/models/workspace.model.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "class-transformer"; 2 | import { 3 | IsArray, 4 | IsBoolean, 5 | IsDate, 6 | IsEmail, 7 | IsEnum, 8 | IsMongoId, 9 | IsNotEmpty, 10 | IsObject, 11 | IsOptional, 12 | IsString, 13 | ValidateNested, 14 | } from "class-validator"; 15 | import { CollectionDto } from "./collection.model"; 16 | import { ObjectId } from "mongodb"; 17 | import { EnvironmentDto } from "./environment.model"; 18 | import { TestflowInfoDto } from "./testflow.model"; 19 | 20 | export enum WorkspaceType { 21 | PRIVATE = "PRIVATE", 22 | PUBLIC = "PUBLIC", 23 | } 24 | 25 | export class UserDto { 26 | @IsMongoId() 27 | @IsNotEmpty() 28 | id: string; 29 | 30 | @IsNotEmpty() 31 | @IsString() 32 | role: string; 33 | 34 | @IsNotEmpty() 35 | @IsString() 36 | name: string; 37 | 38 | @IsEmail() 39 | @IsNotEmpty() 40 | email: string; 41 | } 42 | 43 | export class AdminDto { 44 | @IsMongoId() 45 | @IsNotEmpty() 46 | id: string; 47 | 48 | @IsNotEmpty() 49 | @IsString() 50 | name: string; 51 | } 52 | 53 | export class OwnerInformationDto { 54 | @IsMongoId() 55 | @IsNotEmpty() 56 | id: string; 57 | 58 | @IsString() 59 | @IsNotEmpty() 60 | name?: string; 61 | 62 | @IsEnum(WorkspaceType) 63 | @IsNotEmpty() 64 | type: WorkspaceType; 65 | } 66 | 67 | export class TeamInfoDto { 68 | @IsMongoId() 69 | @IsNotEmpty() 70 | id: string; 71 | 72 | @IsString() 73 | @IsNotEmpty() 74 | name: string; 75 | 76 | @IsString() 77 | @IsOptional() 78 | hubUrl?: string; 79 | } 80 | 81 | export class Workspace { 82 | @IsString() 83 | @IsNotEmpty() 84 | name: string; 85 | 86 | @IsNotEmpty() 87 | @IsObject() 88 | team: TeamInfoDto; 89 | 90 | @IsString() 91 | @IsOptional() 92 | description?: string; 93 | 94 | @IsString() 95 | @IsOptional() 96 | @IsEnum(WorkspaceType) 97 | workspaceType?: WorkspaceType; 98 | 99 | @IsArray() 100 | @Type(() => CollectionDto) 101 | @ValidateNested({ each: true }) 102 | @IsOptional() 103 | collection?: CollectionDto[]; 104 | 105 | @IsArray() 106 | @Type(() => EnvironmentDto) 107 | @ValidateNested({ each: true }) 108 | @IsOptional() 109 | environments?: EnvironmentDto[]; 110 | 111 | @IsArray() 112 | @Type(() => TestflowInfoDto) 113 | @ValidateNested({ each: true }) 114 | @IsOptional() 115 | testflows?: TestflowInfoDto[]; 116 | 117 | @IsArray() 118 | @Type(() => AdminDto) 119 | @ValidateNested({ each: true }) 120 | @IsOptional() 121 | admins?: AdminDto[]; 122 | 123 | @IsArray() 124 | @Type(() => UserDto) 125 | @ValidateNested({ each: true }) 126 | @IsOptional() 127 | users?: UserDto[]; 128 | 129 | @IsDate() 130 | @IsOptional() 131 | createdAt?: Date; 132 | 133 | @IsDate() 134 | @IsOptional() 135 | updatedAt?: Date; 136 | 137 | @IsString() 138 | @IsOptional() 139 | createdBy?: string; 140 | 141 | @IsString() 142 | @IsOptional() 143 | updatedBy?: string; 144 | } 145 | 146 | export class WorkspaceWithNewInviteTag extends Workspace { 147 | @IsBoolean() 148 | @IsOptional() 149 | isNewInvite?: boolean; 150 | } 151 | 152 | export class WorkspaceDto { 153 | @IsMongoId() 154 | @IsNotEmpty() 155 | id: ObjectId; 156 | 157 | @IsString() 158 | @IsNotEmpty() 159 | name: string; 160 | } 161 | -------------------------------------------------------------------------------- /src/modules/common/services/api-response.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatusCode } from "../enum/httpStatusCode.enum"; 2 | 3 | export class ApiResponseService { 4 | message: string; 5 | httpStatusCode: HttpStatusCode; 6 | data: any; 7 | 8 | constructor(message: string, httpStatusCode: HttpStatusCode, data?: any) { 9 | this.message = message; 10 | this.httpStatusCode = httpStatusCode; 11 | this.data = data ?? {}; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/common/services/context.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | 3 | @Injectable() 4 | export class ContextService { 5 | private contextData: Record = {}; 6 | 7 | set(key: string, value: any) { 8 | this.contextData[key] = value; 9 | } 10 | 11 | get(key: string) { 12 | return this.contextData[key]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/common/services/email.service.ts: -------------------------------------------------------------------------------- 1 | import * as nodemailer from "nodemailer"; 2 | import hbs from "nodemailer-express-handlebars"; 3 | import * as path from "path"; 4 | import { ConfigService } from "@nestjs/config"; 5 | import { Injectable } from "@nestjs/common"; 6 | import SMTPTransport from "nodemailer/lib/smtp-transport"; 7 | 8 | interface MailOptions { 9 | from: string; 10 | to: string; 11 | text?: string; 12 | template: string; 13 | context?: { 14 | [key: string]: any; 15 | }; 16 | subject: string; 17 | } 18 | 19 | @Injectable() 20 | export class EmailService { 21 | constructor(private configService: ConfigService) {} 22 | 23 | createTransporter() { 24 | const transporter = nodemailer.createTransport({ 25 | host: this.configService.get("app.mailHost"), 26 | port: this.configService.get("app.mailPort"), 27 | secure: this.configService.get("app.mailSecure") === "true", 28 | auth: { 29 | user: this.configService.get("app.userName"), 30 | pass: this.configService.get("app.senderPassword"), 31 | }, 32 | }); 33 | 34 | const handlebarOptions = { 35 | viewEngine: { 36 | extname: ".handlebars", 37 | partialsDir: path.resolve(__dirname, "..", "..", "views", "partials"), 38 | layoutsDir: path.resolve(__dirname, "..", "..", "views", "layouts"), 39 | defaultLayout: "main", 40 | helpers: { 41 | linkedinUrl: () => this.configService.get("social.linkedinUrl"), 42 | githubUrl: () => this.configService.get("social.githubUrl"), 43 | discordUrl: () => this.configService.get("social.discordUrl"), 44 | }, 45 | }, 46 | viewPath: path.resolve(__dirname, "..", "..", "views"), 47 | extName: ".handlebars", 48 | }; 49 | 50 | transporter.use("compile", hbs(handlebarOptions)); 51 | 52 | return transporter; 53 | } 54 | 55 | async sendEmail( 56 | transporter: nodemailer.Transporter< 57 | SMTPTransport.SentMessageInfo, 58 | SMTPTransport.Options 59 | >, 60 | mailOptions: MailOptions, 61 | ): Promise { 62 | const smtpEnabled = this.configService.get("app.smtpEnabled"); 63 | 64 | // Exit early if SMTP is disabled 65 | if (smtpEnabled !== "true") { 66 | console.warn("SMTP is disabled. Email not sent."); 67 | return; 68 | } 69 | 70 | try { 71 | const result = await transporter.sendMail(mailOptions); 72 | return result; // Return the result for further processing if needed 73 | } catch (error) { 74 | console.error("Failed to send email:", error); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/modules/common/services/helper/parser.helper.ts: -------------------------------------------------------------------------------- 1 | export function resolveAllRefs(spec: any) { 2 | if (!spec) return spec; 3 | 4 | if (typeof spec === "object") { 5 | const componentToResolve = spec.components ?? spec.definitions; 6 | const resolvedSpec: { [key: string]: any } = {}; 7 | for (const key in spec) { 8 | if (componentToResolve.schemas) { 9 | resolvedSpec[key] = resolveComponentRef( 10 | spec[key], 11 | componentToResolve, 12 | [], 13 | ); 14 | } else { 15 | resolvedSpec[key] = resolveDefinitionRef(spec[key], componentToResolve); 16 | } 17 | } 18 | return resolvedSpec; 19 | } 20 | 21 | return spec; 22 | } 23 | 24 | function resolveComponentRef(data: any, components: any, cache: any): any { 25 | if (!data) return data; 26 | 27 | if (typeof data === "object") { 28 | if (data.hasOwnProperty("$ref") && typeof data["$ref"] === "string") { 29 | const refPath = data["$ref"]; 30 | if (refPath.startsWith("#/components/schemas/")) { 31 | const schemaName = refPath.split("/").pop(); 32 | if ( 33 | components && 34 | components.schemas && 35 | components.schemas[schemaName] 36 | ) { 37 | if (cache.indexOf(schemaName) !== -1) { 38 | // Circular reference found, replace with undefined 39 | return undefined; 40 | } 41 | cache.push(schemaName); 42 | return resolveComponentRef( 43 | components.schemas[schemaName], 44 | components, 45 | cache, 46 | ); 47 | } else { 48 | console.warn(`Reference "${refPath}" not found in components`); 49 | return data; 50 | } 51 | } else { 52 | return data; 53 | } 54 | } else { 55 | const newData: { [key: string]: any } = {}; 56 | for (const key in data) { 57 | newData[key] = resolveComponentRef(data[key], components, cache); 58 | } 59 | return newData; 60 | } 61 | } 62 | 63 | return data; 64 | } 65 | 66 | function resolveDefinitionRef(data: any, definitions: any): any { 67 | if (!data) return data; 68 | 69 | if (typeof data === "object") { 70 | if (data.hasOwnProperty("$ref") && typeof data["$ref"] === "string") { 71 | const refPath = data["$ref"]; 72 | if (refPath.startsWith("#/definitions/")) { 73 | const schemaName = refPath.split("/").pop(); 74 | if (definitions && definitions[schemaName]) { 75 | return resolveDefinitionRef(definitions[schemaName], definitions); 76 | } else { 77 | console.warn(`Reference "${refPath}" not found in definitions`); 78 | return data; 79 | } 80 | } else { 81 | return data; 82 | } 83 | } else { 84 | const newData: { [key: string]: any } = {}; 85 | for (const key in data) { 86 | newData[key] = resolveDefinitionRef(data[key], definitions); 87 | } 88 | return newData; 89 | } 90 | } 91 | 92 | return data; 93 | } 94 | -------------------------------------------------------------------------------- /src/modules/common/services/insights.service.ts: -------------------------------------------------------------------------------- 1 | // insights.service.ts 2 | /** 3 | * The `InsightsService` class provides a wrapper around Microsoft Application Insights, 4 | * allowing for telemetry data to be sent to Azure from your NestJS application. This service 5 | * is responsible for initializing Application Insights and providing access to the telemetry client. 6 | * 7 | * @class InsightsService 8 | */ 9 | import { Injectable } from "@nestjs/common"; 10 | import { ConfigService } from "@nestjs/config"; 11 | import * as appInsights from "applicationinsights"; 12 | 13 | @Injectable() 14 | export class InsightsService { 15 | private readonly client = appInsights.defaultClient; 16 | /** 17 | * Creates an instance of `InsightsService`. 18 | * The constructor initializes Application Insights if it hasn't been initialized already. 19 | * 20 | * @param {ConfigService} configService - The NestJS `ConfigService` used to retrieve the 21 | * Azure Application Insights connection string from environment variables. 22 | */ 23 | constructor(private configService: ConfigService) { 24 | // Ensure Application Insights is initialized only once 25 | const azureInsightsConnectionString = this.configService.get( 26 | "azure.insightsConnectionString", 27 | ); 28 | if (!this.client) { 29 | try { 30 | appInsights 31 | .setup(azureInsightsConnectionString) 32 | .setAutoDependencyCorrelation(true) 33 | .setAutoCollectRequests(true) 34 | .setAutoCollectPerformance(true, true) 35 | .setAutoCollectExceptions(true) 36 | .setAutoCollectDependencies(true) 37 | .setAutoCollectConsole(true) 38 | .setUseDiskRetryCaching(true) 39 | .setSendLiveMetrics(true) 40 | .start(); 41 | this.client = appInsights.defaultClient; 42 | } catch (e) { 43 | console.error(e); 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * Retrieves the Application Insights telemetry client. 50 | * 51 | * @returns {appInsights.TelemetryClient} - The Application Insights telemetry client. 52 | */ 53 | getClient() { 54 | return this.client; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/common/services/instrument.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import * as Sentry from "@sentry/nestjs"; 3 | import { ConfigService } from "@nestjs/config"; 4 | 5 | @Injectable() 6 | export class InstrumentService { 7 | constructor(private readonly configService: ConfigService) { 8 | // Retrieve Sentry configuration using ConfigService 9 | const sentryDsn = this.configService.get("sentry.dsn"); 10 | const sentryEnvironment = 11 | this.configService.get("sentry.environment"); 12 | 13 | if (sentryEnvironment !== "LOCAL-BE" && sentryDsn && sentryEnvironment) { 14 | Sentry.init({ 15 | dsn: sentryDsn, 16 | environment: sentryEnvironment, 17 | beforeSend: (event) => { 18 | return event; 19 | }, 20 | // Setting this option to true will send default PII data to Sentry. 21 | sendDefaultPii: true, 22 | }); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/common/services/kafka/consumer.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IConsumer { 2 | connect: () => Promise; 3 | disconnect: () => Promise; 4 | consume: (message: any) => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/common/services/kafka/consumer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnApplicationShutdown } from "@nestjs/common"; 2 | import { ConfigService } from "@nestjs/config"; 3 | import { 4 | ConsumerConfig, 5 | ConsumerSubscribeTopic, 6 | KafkaJSError, 7 | KafkaMessage, 8 | } from "kafkajs"; 9 | import { IConsumer } from "./consumer.interface"; 10 | import { KafkajsConsumer } from "./kafkajs.consumer"; 11 | 12 | interface KafkajsConsumerOptions { 13 | topic: ConsumerSubscribeTopic; 14 | config: ConsumerConfig; 15 | onMessage: (message: KafkaMessage) => Promise; 16 | onError: (error: KafkaJSError) => Promise; 17 | } 18 | 19 | @Injectable() 20 | export class ConsumerService implements OnApplicationShutdown { 21 | private readonly consumers: IConsumer[] = []; 22 | 23 | constructor(private readonly configService: ConfigService) {} 24 | 25 | async consume({ topic, config, onMessage }: KafkajsConsumerOptions) { 26 | const kafkaBroker = [this.configService.get("kafka.broker")]; 27 | const consumer = new KafkajsConsumer(topic, config, kafkaBroker); 28 | await consumer.connect(); 29 | await consumer.consume(onMessage); 30 | this.consumers.push(consumer); 31 | } 32 | 33 | async onApplicationShutdown() { 34 | for (const consumer of this.consumers) { 35 | await consumer.disconnect(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/modules/common/services/kafka/kafkajs.consumer.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "@nestjs/common"; 2 | import { 3 | Admin, 4 | Consumer, 5 | ConsumerConfig, 6 | ConsumerSubscribeTopic, 7 | Kafka, 8 | KafkaMessage, 9 | } from "kafkajs"; 10 | import * as retry from "async-retry"; 11 | import { IConsumer } from "./consumer.interface"; 12 | 13 | export class KafkajsConsumer implements IConsumer { 14 | private readonly kafka: Kafka; 15 | private readonly consumer: Consumer; 16 | private readonly logger: Logger; 17 | private readonly admin: Admin; 18 | 19 | constructor( 20 | private readonly topic: ConsumerSubscribeTopic, 21 | config: ConsumerConfig, 22 | brokers: string[], 23 | ) { 24 | this.kafka = new Kafka({ brokers }); 25 | this.consumer = this.kafka.consumer(config); 26 | this.logger = new Logger(`${topic.topic}-${config.groupId}`); 27 | this.admin = this.kafka.admin(); 28 | } 29 | 30 | async consume(onMessage: (message: KafkaMessage) => Promise) { 31 | try { 32 | const topics = await this.admin.listTopics(); 33 | const topicToAdd = this.topic.topic as string; 34 | if (!topics.includes(topicToAdd)) { 35 | await this.createTopic(topicToAdd); 36 | } 37 | await this.consumer.subscribe(this.topic); 38 | await this.consumer.run({ 39 | eachMessage: async ({ message, partition }) => { 40 | this.logger.debug(`Processing message partition: ${partition}`); 41 | try { 42 | await retry.default(async () => onMessage(message), { 43 | retries: 3, 44 | onRetry: (error: any, attempt: number) => 45 | this.logger.error( 46 | `Error consuming message, executing retry ${attempt}/3...`, 47 | error, 48 | ), 49 | }); 50 | } catch (err) { 51 | this.logger.error( 52 | "Error consuming message. Adding to dead letter queue...", 53 | err, 54 | ); 55 | } 56 | }, 57 | }); 58 | } catch (e) { 59 | this.logger.error("Error creating topics/consumption", e); 60 | } finally { 61 | await this.admin.disconnect(); 62 | } 63 | } 64 | 65 | async connect() { 66 | try { 67 | await this.consumer.connect(); 68 | } catch (err) { 69 | this.logger.error("Failed to connect to Kafka.", err); 70 | await this.connect(); 71 | } 72 | } 73 | 74 | async disconnect() { 75 | await this.consumer.disconnect(); 76 | } 77 | 78 | async createTopic(topic: string): Promise { 79 | await this.admin.connect(); 80 | await this.admin.createTopics({ 81 | topics: [{ topic }], 82 | waitForLeaders: true, 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/modules/common/services/kafka/kafkajs.producer.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "@nestjs/common"; 2 | import { Kafka, Message, Producer } from "kafkajs"; 3 | import { IProducer } from "./producer.interface"; 4 | 5 | export class KafkajsProducer implements IProducer { 6 | private readonly kafka: Kafka; 7 | private readonly producer: Producer; 8 | private readonly logger: Logger; 9 | 10 | constructor( 11 | private readonly topic: string, 12 | brokers: string[], 13 | ) { 14 | this.kafka = new Kafka({ 15 | brokers, 16 | }); 17 | this.producer = this.kafka.producer(); 18 | this.logger = new Logger(topic); 19 | } 20 | 21 | async produce(message: Message) { 22 | await this.producer.send({ topic: this.topic, messages: [message] }); 23 | } 24 | 25 | async connect() { 26 | try { 27 | await this.producer.connect(); 28 | } catch (err) { 29 | this.logger.error("Failed to connect to Kafka.", err); 30 | await this.connect(); 31 | } 32 | } 33 | 34 | /** 35 | * This function is to check if kafka is able to connect and send message or not. 36 | * @returns Boolean value to reflect if kafka is connected or not. 37 | */ 38 | async isKafkaConnected(): Promise { 39 | try { 40 | await this.connect(); 41 | if (!this.producer) { 42 | console.log("Kafka producer is not initialized."); 43 | return false; 44 | } 45 | 46 | // Try sending a dummy message to verify Kafka connection 47 | await this.producer.send({ 48 | topic: this.topic, 49 | messages: [{ key: "health-check", value: "ping" }], 50 | }); 51 | 52 | return true; 53 | } catch (error) { 54 | console.log("Kafka connection error:", error); 55 | return false; 56 | } 57 | } 58 | 59 | async disconnect() { 60 | await this.producer.disconnect(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/modules/common/services/kafka/producer.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IProducer { 2 | connect: () => Promise; 3 | disconnect: () => Promise; 4 | produce: (message: any) => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/common/services/kafka/producer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnApplicationShutdown } from "@nestjs/common"; 2 | import { ConfigService } from "@nestjs/config"; 3 | import { Message } from "kafkajs"; 4 | import { KafkajsProducer } from "./kafkajs.producer"; 5 | import { IProducer } from "./producer.interface"; 6 | 7 | @Injectable() 8 | export class ProducerService implements OnApplicationShutdown { 9 | private readonly producers = new Map(); 10 | 11 | constructor(private readonly configService: ConfigService) {} 12 | 13 | async produce(topic: string, message: Message) { 14 | const producer = await this.getProducer(topic); 15 | await producer.produce(message); 16 | } 17 | 18 | private async getProducer(topic: string) { 19 | let producer = this.producers.get(topic); 20 | if (!producer) { 21 | const kafkaBroker = [this.configService.get("kafka.broker")]; 22 | producer = new KafkajsProducer(topic, kafkaBroker); 23 | await producer.connect(); 24 | this.producers.set(topic, producer); 25 | } 26 | return producer; 27 | } 28 | 29 | async onApplicationShutdown() { 30 | for (const producer of this.producers.values()) { 31 | await producer.disconnect(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/common/services/postman.parser.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | 3 | // ---- Model 4 | import { 5 | Collection, 6 | CollectionAuthModeEnum, 7 | } from "@common/models/collection.model"; 8 | 9 | // ---- Services 10 | import { ContextService } from "./context.service"; 11 | 12 | // ---- Helpers 13 | import { 14 | convertItems, 15 | countTotalRequests, 16 | flattenPostmanCollection, 17 | } from "./helper/postman.parser"; 18 | 19 | @Injectable() 20 | export class PostmanParserService { 21 | constructor(private readonly contextService: ContextService) {} 22 | /** 23 | * Parses a Postman Collection, converts the items, and enriches them with user information. 24 | * Then, it creates a collection object and flattens the collection before returning it. 25 | * 26 | * @param postmanCollection - The Postman Collection object to be parsed. 27 | * @returns The processed and flattened Postman Collection. 28 | */ 29 | async parsePostmanCollection(postmanCollection: any) { 30 | const user = await this.contextService.get("user"); 31 | 32 | // Destructure the 'info' and 'item' properties from the Postman collection 33 | const { info, item: items } = 34 | postmanCollection.collection ?? postmanCollection; 35 | 36 | // Convert the items of the Postman collection into a specific format 37 | // Majorly responsible for converting the folder and request structure of postman into Sparrow's structure 38 | let convertedItems = convertItems(items); 39 | convertedItems = convertedItems.map((item) => { 40 | item.createdBy = user?.name ?? ""; 41 | items.updatedBy = user?.name ?? ""; 42 | return item; 43 | }); 44 | 45 | // Build the collection object with the parsed data and user information 46 | const collection: Collection = { 47 | name: info.name, 48 | description: info.description ?? "", 49 | items: convertedItems, 50 | selectedAuthType: CollectionAuthModeEnum["No Auth"], 51 | totalRequests: countTotalRequests(items), 52 | createdBy: user?.name, 53 | updatedBy: { 54 | id: user?._id ?? "", 55 | name: user?.name ?? "", 56 | }, 57 | createdAt: new Date(), 58 | updatedAt: new Date(), 59 | }; 60 | 61 | // Flatten the Postman collection to resolve nested folder issue and return the updated collection 62 | const updatedCollection = await flattenPostmanCollection(collection); 63 | return updatedCollection; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/modules/common/util/email.parser.util.ts: -------------------------------------------------------------------------------- 1 | export function parseWhitelistedEmailList(str: string) { 2 | // Replace single quotes with double quotes to make it valid JSON 3 | const jsonCompatible = str.replace(/'/g, '"'); 4 | 5 | try { 6 | const emailArray = JSON.parse(jsonCompatible); 7 | if (Array.isArray(emailArray)) { 8 | return emailArray; 9 | } 10 | } catch (err) { 11 | console.log("Failed to parse email list:", err); 12 | return []; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/common/util/sleep.util.ts: -------------------------------------------------------------------------------- 1 | export default function sleep(timeout: number) { 2 | return new Promise((resolve) => setTimeout(resolve, timeout)); 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/identity/controllers/test/mockData/auth.payload.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "mongodb"; 2 | 3 | export const LoginPayload = { 4 | email: "test@email.com", 5 | password: "testpassword", 6 | }; 7 | 8 | export const LoginResponse = { 9 | message: "Login Successful", 10 | httpStatusCode: 200, 11 | data: { 12 | accessToken: { 13 | expires: "2400", 14 | expiresPrettyPrint: " 40 minutes ", 15 | token: 16 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NWE1OGZjNjdiYTI5NTMzMTgwMjMyMGYiLCJlbWFpbCI6InVzZXJAZW1haWwuY29tIiwicGVyc29uYWxXb3Jrc3BhY2VzIjpbeyJ3b3Jrc3BhY2VJZCI6IjY1YTU4ZmM2N2JhMjk1MzMxODAyMzIxMiIsIm5hbWUiOiJ1c2VybmFtZSJ9XSwiZXhwIjoxNzA1MzUxODk4LjMyMiwiaWF0IjoxNzA1MzQ5NDk4fQ.ano330i-eesUGDKW-uwBE47xHR0l5WdJhDbBzXDc_rE", 17 | }, 18 | refreshToken: { 19 | expires: "604800", 20 | expiresPrettyPrint: "168 hours, ", 21 | token: 22 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NWE1OGZjNjdiYTI5NTMzMTgwMjMyMGYiLCJlbWFpbCI6InVzZXJAZW1haWwuY29tIiwiZXhwIjoxNzA1OTU0Mjk4LjMyNSwiaWF0IjoxNzA1MzQ5NDk4fQ.G8WW0kXfKCwr48_yGGuKLzDfgnG8qWsbfEBMsluE3Ec", 23 | }, 24 | }, 25 | }; 26 | 27 | export const TokenResponse = { 28 | expires: "2400", 29 | expiresPrettyPrint: " 40 minutes ", 30 | token: 31 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NWE1OGZjNjdiYTI5NTMzMTgwMjMyMGYiLCJlbWFpbCI6InVzZXJAZW1haWwuY29tIiwicGVyc29uYWxXb3Jrc3BhY2VzIjpbeyJ3b3Jrc3BhY2VJZCI6IjY1YTU4ZmM2N2JhMjk1MzMxODAyMzIxMiIsIm5hbWUiOiJ1c2VybmFtZSJ9XSwiZXhwIjoxNzA1MzUyMzE3Ljc1OCwiaWF0IjoxNzA1MzQ5OTE3fQ.aHS_oJSgXoU4sMvnOtIjqdKMdvCILxqn8pR3wVUw2J0", 32 | }; 33 | 34 | export const RefreshTokenRequest = { 35 | user: { 36 | _id: "", 37 | refreshToken: "", 38 | }, 39 | }; 40 | 41 | export const RefreshTokenResponse = { 42 | message: "Token Generated", 43 | httpStatusCode: 200, 44 | data: { 45 | newAccessToken: TokenResponse, 46 | newRefreshToken: TokenResponse, 47 | }, 48 | }; 49 | 50 | export const OauthUserDetails = { 51 | user: { 52 | oAuthId: "", 53 | name: "", 54 | email: "", 55 | }, 56 | }; 57 | 58 | export const UserDetails = { 59 | _id: new ObjectId("mockedUserId"), 60 | name: "test", 61 | email: "test@email.com", 62 | teams: [] as any, 63 | personalWorkspaces: [] as any, 64 | }; 65 | 66 | export const MockUrl = "https://mock.com"; 67 | -------------------------------------------------------------------------------- /src/modules/identity/identity.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { ConfigModule, ConfigService } from "@nestjs/config"; 3 | import { JwtModule, JwtService } from "@nestjs/jwt"; 4 | import { PassportModule } from "@nestjs/passport"; 5 | import { AuthController } from "./controllers/auth.controller"; 6 | import { AuthService } from "./services/auth.service"; 7 | import { JwtStrategy } from "./strategies/jwt.strategy"; 8 | import { UserService } from "./services/user.service"; 9 | import { UserRepository } from "./repositories/user.repository"; 10 | import { UserController } from "./controllers/user.controller"; 11 | import { TeamService } from "./services/team.service"; 12 | import { TeamUserService } from "./services/team-user.service"; 13 | import { TeamRepository } from "./repositories/team.repository"; 14 | import { UserInvitesRepository } from "./repositories/userInvites.repository"; 15 | import { TeamController } from "./controllers/team.controller"; 16 | import { RefreshTokenStrategy } from "./strategies/refresh-token.strategy"; 17 | import { HubSpotService } from "./services/hubspot.service"; 18 | import { GoogleStrategy } from "./strategies/google.strategy"; 19 | 20 | @Module({ 21 | imports: [ 22 | ConfigModule, 23 | PassportModule.register({ defaultStrategy: "jwt" }), 24 | JwtModule.registerAsync({ 25 | imports: [ConfigModule], 26 | useFactory: async (configService: ConfigService) => { 27 | return { 28 | secret: configService.get("app.jwtSecretKey"), 29 | signOptions: { 30 | ...(configService.get("app.jwtExpirationTime") 31 | ? { 32 | expiresIn: Number(configService.get("app.jwtExpirationTime")), 33 | } 34 | : {}), 35 | }, 36 | }; 37 | }, 38 | inject: [ConfigService], 39 | }), 40 | ], 41 | providers: [ 42 | AuthService, 43 | JwtStrategy, 44 | RefreshTokenStrategy, 45 | UserService, 46 | JwtService, 47 | UserRepository, 48 | TeamService, 49 | TeamUserService, 50 | TeamRepository, 51 | { 52 | provide: GoogleStrategy, 53 | useFactory: (configService: ConfigService) => { 54 | const isGoogleAuthEnabled = configService.get( 55 | "oauth.google.enableGoogleAuth", 56 | "true", 57 | ); 58 | return isGoogleAuthEnabled === "true" 59 | ? new GoogleStrategy(configService) 60 | : null; 61 | }, 62 | inject: [ConfigService], 63 | }, 64 | HubSpotService, 65 | UserInvitesRepository, 66 | ], 67 | exports: [ 68 | PassportModule.register({ defaultStrategy: "jwt" }), 69 | AuthService, 70 | UserService, 71 | UserRepository, 72 | TeamService, 73 | TeamUserService, 74 | TeamRepository, 75 | UserInvitesRepository, 76 | HubSpotService, 77 | ], 78 | controllers: [AuthController, UserController, TeamController], 79 | }) 80 | export class IdentityModule {} 81 | -------------------------------------------------------------------------------- /src/modules/identity/payloads/jwt.payload.ts: -------------------------------------------------------------------------------- 1 | export class JwtPayload { 2 | iat: number; 3 | exp: number; 4 | _id: string; 5 | role: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/identity/payloads/login.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { IsEmail, IsNotEmpty, MinLength } from "class-validator"; 3 | 4 | /** 5 | * Login Paylaod Class 6 | */ 7 | export class LoginPayload { 8 | /** 9 | * Email field 10 | */ 11 | @ApiProperty({ 12 | required: true, 13 | example: "user@email.com", 14 | }) 15 | @IsEmail() 16 | @IsNotEmpty() 17 | email: string; 18 | 19 | /** 20 | * Password field 21 | */ 22 | @ApiProperty({ 23 | required: true, 24 | example: "userpassword", 25 | }) 26 | @IsNotEmpty() 27 | @MinLength(8) 28 | password: string; 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/identity/payloads/register.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { 3 | IsEmail, 4 | IsNotEmpty, 5 | MinLength, 6 | Matches, 7 | IsBoolean, 8 | IsOptional, 9 | } from "class-validator"; 10 | 11 | /** 12 | * Register Payload Class 13 | */ 14 | export class RegisterPayload { 15 | /** 16 | * Email field 17 | */ 18 | @ApiProperty({ 19 | required: true, 20 | example: "user@email.com", 21 | }) 22 | @IsEmail() 23 | @IsNotEmpty() 24 | email: string; 25 | 26 | /** 27 | * Name field 28 | */ 29 | @ApiProperty({ 30 | required: true, 31 | example: "username", 32 | }) 33 | @Matches(/^[a-zA-Z ]+$/, { 34 | message: "username only contain characters.", 35 | }) 36 | @IsNotEmpty() 37 | name: string; 38 | 39 | /** 40 | * Password field 41 | */ 42 | @ApiProperty({ 43 | required: true, 44 | example: "userpassword", 45 | }) 46 | @IsNotEmpty() 47 | @Matches(/(?=.*[0-9])/, { 48 | message: "password must contain at least one digit.", 49 | }) 50 | @Matches(/(?=.*[!@#$%^&*])/, { 51 | message: "password must contain at least one special character (!@#$%^&*).", 52 | }) 53 | @MinLength(8) 54 | password: string; 55 | 56 | @ApiProperty({ 57 | required: true, 58 | example: true, 59 | }) 60 | @IsOptional() 61 | @IsBoolean() 62 | isUserAcceptedOccasionalUpdates?: boolean; 63 | } 64 | -------------------------------------------------------------------------------- /src/modules/identity/payloads/resetPassword.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { IsEmail, IsNotEmpty, Matches, MinLength } from "class-validator"; 3 | 4 | export class ResetPasswordPayload { 5 | @ApiProperty({ 6 | required: true, 7 | example: "user@email.com", 8 | }) 9 | @IsEmail() 10 | @IsNotEmpty() 11 | email: string; 12 | } 13 | 14 | export class EarlyAccessPayload { 15 | @ApiProperty({ 16 | required: true, 17 | example: "user@email.com", 18 | }) 19 | @IsEmail() 20 | @IsNotEmpty() 21 | email: string; 22 | } 23 | 24 | export class VerifyEmailPayload { 25 | @ApiProperty({ 26 | example: "user@email.com", 27 | required: true, 28 | }) 29 | @IsEmail() 30 | @IsNotEmpty() 31 | email: string; 32 | @ApiProperty({ 33 | required: true, 34 | example: "ABC123", 35 | }) 36 | @MinLength(6) 37 | @IsNotEmpty() 38 | verificationCode: string; 39 | } 40 | export class UpdatePasswordPayload { 41 | @ApiProperty({ 42 | required: true, 43 | example: "user@email.com", 44 | }) 45 | @IsEmail() 46 | @IsNotEmpty() 47 | email: string; 48 | @ApiProperty({ 49 | required: true, 50 | example: "newPassword", 51 | }) 52 | @IsNotEmpty() 53 | @Matches(/(?=.*[0-9])/, { 54 | message: "password must contain at least one digit.", 55 | }) 56 | @Matches(/(?=.*[!@#$%^&*])/, { 57 | message: "password must contain at least one special character (!@#$%^&*).", 58 | }) 59 | @MinLength(8) 60 | newPassword: string; 61 | 62 | @ApiProperty({ 63 | required: true, 64 | example: "ABC123", 65 | }) 66 | @MinLength(6) 67 | @IsNotEmpty() 68 | verificationCode: string; 69 | } 70 | -------------------------------------------------------------------------------- /src/modules/identity/payloads/team.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { 3 | IsArray, 4 | IsBoolean, 5 | IsNotEmpty, 6 | IsNumber, 7 | IsObject, 8 | IsOptional, 9 | IsString, 10 | } from "class-validator"; 11 | import { Type } from "class-transformer"; 12 | import { WorkspaceDto } from "@src/modules/common/models/workspace.model"; 13 | import { UserDto } from "@src/modules/common/models/user.model"; 14 | import { Invite } from "@src/modules/common/models/team.model"; 15 | 16 | export class logoDto { 17 | @IsString() 18 | @IsNotEmpty() 19 | bufferString: string; 20 | 21 | @IsString() 22 | @IsNotEmpty() 23 | encoding: string; 24 | 25 | @IsString() 26 | @IsNotEmpty() 27 | mimetype: string; 28 | 29 | @IsNumber() 30 | @IsNotEmpty() 31 | size: number; 32 | } 33 | 34 | export class CreateOrUpdateTeamDto { 35 | @ApiProperty({ 36 | example: "team1", 37 | }) 38 | @IsString() 39 | @IsNotEmpty() 40 | name: string; 41 | 42 | @ApiProperty({ 43 | example: "Description of Team", 44 | }) 45 | @IsString() 46 | @IsOptional() 47 | description?: string; 48 | 49 | @IsString() 50 | @IsOptional() 51 | hubUrl?: string; 52 | 53 | @IsString() 54 | @IsOptional() 55 | githubUrl?: string; 56 | 57 | @IsString() 58 | @IsOptional() 59 | xUrl?: string; 60 | 61 | @IsString() 62 | @IsOptional() 63 | linkedinUrl?: string; 64 | 65 | @IsBoolean() 66 | @IsOptional() 67 | firstTeam?: boolean; 68 | 69 | @IsOptional() 70 | @IsObject() 71 | logo?: logoDto; 72 | } 73 | 74 | export class TeamDto { 75 | @IsOptional() 76 | @IsString() 77 | name?: string; 78 | 79 | @IsArray() 80 | @Type(() => WorkspaceDto) 81 | @IsOptional() 82 | workspaces?: WorkspaceDto[]; 83 | 84 | @IsArray() 85 | @Type(() => UserDto) 86 | @IsOptional() 87 | users?: UserDto[]; 88 | 89 | @IsArray() 90 | @IsOptional() 91 | owner?: string; 92 | 93 | @IsArray() 94 | @IsOptional() 95 | admins?: string[]; 96 | 97 | @IsArray() 98 | @IsOptional() 99 | invites?: Invite[]; 100 | } 101 | 102 | export class UpdateTeamDto { 103 | @ApiProperty({ 104 | example: "team1", 105 | }) 106 | @IsOptional() 107 | name?: string; 108 | 109 | @ApiProperty({ 110 | example: "Description of Team", 111 | }) 112 | @IsString() 113 | @IsOptional() 114 | description?: string; 115 | 116 | @ApiProperty({ 117 | example: "github url", 118 | }) 119 | @IsString() 120 | @IsOptional() 121 | githubUrl?: string; 122 | 123 | @ApiProperty({ 124 | example: "x url", 125 | }) 126 | @IsString() 127 | @IsOptional() 128 | xUrl?: string; 129 | 130 | @ApiProperty({ 131 | example: "LinkedIn url", 132 | }) 133 | @IsString() 134 | @IsOptional() 135 | linkedinUrl?: string; 136 | 137 | @IsOptional() 138 | @IsObject() 139 | logo?: logoDto; 140 | } 141 | -------------------------------------------------------------------------------- /src/modules/identity/payloads/teamUser.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { User, UserDto } from "@src/modules/common/models/user.model"; 3 | import { WorkspaceDto } from "@src/modules/common/models/workspace.model"; 4 | import { Type } from "class-transformer"; 5 | import { 6 | IsNotEmpty, 7 | IsMongoId, 8 | IsString, 9 | IsOptional, 10 | IsArray, 11 | IsDateString, 12 | ValidateNested, 13 | } from "class-validator"; 14 | 15 | export class CreateOrUpdateTeamUserDto { 16 | @ApiProperty({ example: "64f03af32e420f7f68055b92" }) 17 | @IsMongoId() 18 | @IsNotEmpty() 19 | teamId: string; 20 | 21 | @ApiProperty({ example: "64f03af32e420f7f68055b92" }) 22 | @IsMongoId() 23 | @IsNotEmpty() 24 | userId: string; 25 | } 26 | 27 | export class CreateOrUpdateTeamUserResponseDto { 28 | @IsMongoId() 29 | @IsNotEmpty() 30 | @IsOptional() 31 | id?: string; 32 | 33 | @IsOptional() 34 | @IsString() 35 | name?: string; 36 | 37 | @IsArray() 38 | @Type(() => WorkspaceDto) 39 | @IsOptional() 40 | workspaces?: WorkspaceDto[]; 41 | 42 | @IsArray() 43 | @Type(() => UserDto) 44 | @IsOptional() 45 | users?: UserDto[]; 46 | 47 | @IsArray() 48 | @IsOptional() 49 | owners?: string[]; 50 | 51 | @IsMongoId() 52 | @IsNotEmpty() 53 | @IsOptional() 54 | createdBy?: string; 55 | 56 | @IsDateString() 57 | @IsOptional() 58 | createdAt?: Date; 59 | 60 | @IsDateString() 61 | @IsOptional() 62 | updatedAt?: Date; 63 | } 64 | 65 | export class SelectedWorkspaces { 66 | @IsNotEmpty() 67 | @IsString() 68 | @ApiProperty({ example: "64f03af32e420f7f68055b92" }) 69 | id: string; 70 | 71 | @IsNotEmpty() 72 | @IsString() 73 | @ApiProperty({ example: "MY Workspace" }) 74 | name: string; 75 | } 76 | 77 | export class AddTeamUserDto { 78 | @IsMongoId() 79 | @IsOptional() 80 | teamId?: string; 81 | 82 | @IsArray() 83 | @ApiProperty({ example: ["user@gmail.com"] }) 84 | users: string[]; 85 | 86 | @IsString() 87 | @IsNotEmpty() 88 | @ApiProperty({ example: "admin" }) 89 | role: string; 90 | 91 | @IsArray() 92 | @IsOptional() 93 | @Type(() => SelectedWorkspaces) 94 | @ApiProperty({ type: [SelectedWorkspaces] }) 95 | @ValidateNested({ each: true }) 96 | workspaces?: SelectedWorkspaces[]; 97 | } 98 | 99 | export class AddTeamUserWithSenderDto extends AddTeamUserDto { 100 | @IsString() 101 | @IsOptional() 102 | senderEmail?: string; 103 | } 104 | 105 | export class TeamInviteMailDto { 106 | @IsArray() 107 | @IsNotEmpty() 108 | @Type(() => User) 109 | users: User[]; 110 | 111 | @IsString() 112 | @IsNotEmpty() 113 | teamName: string; 114 | } 115 | -------------------------------------------------------------------------------- /src/modules/identity/payloads/user.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { TeamDto } from "@src/modules/common/models/team.model"; 3 | import { UserWorkspaceDto } from "@src/modules/common/models/user.model"; 4 | import { Type } from "class-transformer"; 5 | import { 6 | IsArray, 7 | IsBoolean, 8 | IsEmail, 9 | IsNotEmpty, 10 | IsOptional, 11 | IsString, 12 | Matches, 13 | MinLength, 14 | ValidateNested, 15 | // ValidateNested, 16 | } from "class-validator"; 17 | 18 | export class UpdateUserDto { 19 | /** 20 | * Name field 21 | */ 22 | @ApiProperty({ 23 | required: true, 24 | example: "username", 25 | }) 26 | @Matches(/^[a-zA-Z ]+$/) 27 | @IsNotEmpty() 28 | name: string; 29 | } 30 | 31 | export class UserDto { 32 | @IsString() 33 | @IsOptional() 34 | name?: string; 35 | 36 | @IsEmail() 37 | @IsOptional() 38 | email?: string; 39 | 40 | @IsArray() 41 | @IsOptional() 42 | @Type(() => TeamDto) 43 | teams?: TeamDto[]; 44 | 45 | @IsArray() 46 | @Type(() => UserWorkspaceDto) 47 | @IsOptional() 48 | @ValidateNested({ each: true }) 49 | workspaces?: UserWorkspaceDto[]; 50 | } 51 | 52 | export class RegisteredWith { 53 | @IsString() 54 | registeredWith: string; 55 | } 56 | 57 | /** 58 | * Payload for sending a Magic Code email. 59 | */ 60 | export class EmailPayload { 61 | /** 62 | * The email address to which the Magic Code should be sent. 63 | * @example "user@email.com" 64 | */ 65 | @ApiProperty({ 66 | required: true, 67 | example: "user@email.com", 68 | }) 69 | @IsEmail() 70 | @IsNotEmpty() 71 | email: string; 72 | } 73 | 74 | /** 75 | * Payload for verifying the Magic Code sent to the user's email. 76 | * Extends the `EmailPayload` to include the Magic Code. 77 | */ 78 | export class VerifyMagiCodePayload extends EmailPayload { 79 | /** 80 | * The Magic Code to be verified. 81 | * @example "ABC123" 82 | */ 83 | @ApiProperty({ 84 | required: true, 85 | example: "ABC123", 86 | }) 87 | @MinLength(6) 88 | @IsNotEmpty() 89 | magicCode: string; 90 | } 91 | 92 | export class OccaisonalUpdatesPayload extends EmailPayload { 93 | /** 94 | * The occaisonal uodates status. 95 | * @example true 96 | */ 97 | @ApiProperty({ 98 | required: true, 99 | example: true, 100 | }) 101 | @IsNotEmpty() 102 | @IsBoolean() 103 | isUserAcceptedOccasionalUpdates: boolean; 104 | } 105 | -------------------------------------------------------------------------------- /src/modules/identity/payloads/userInvites.payload.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsOptional, IsArray, IsString } from "class-validator"; 2 | 3 | export class CreateInviteUser { 4 | @IsEmail() 5 | @IsOptional() 6 | email?: string; 7 | 8 | @IsArray() 9 | @IsString({ each: true }) 10 | teamIds: string[]; 11 | } 12 | 13 | export class UpdateInviteUser { 14 | @IsEmail() 15 | @IsOptional() 16 | email?: string; 17 | 18 | @IsArray() 19 | @IsString({ each: true }) 20 | teamIds: string[]; 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/identity/payloads/verification.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { IsEmail, IsNotEmpty } from "class-validator"; 3 | 4 | export class VerificationPayload { 5 | @ApiProperty({ 6 | required: true, 7 | example: "user@email.com", 8 | }) 9 | @IsEmail() 10 | @IsNotEmpty() 11 | email: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/identity/repositories/userInvites.repository.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "@nestjs/common"; 2 | import { Db, InsertOneResult, UpdateResult } from "mongodb"; 3 | import { 4 | CreateInviteUser, 5 | UpdateInviteUser, 6 | } from "../payloads/userInvites.payload"; 7 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 8 | import { UserInvites } from "@src/modules/common/models/user-invites.model"; 9 | 10 | @Injectable() 11 | export class UserInvitesRepository { 12 | constructor( 13 | @Inject("DATABASE_CONNECTION") 14 | private readonly db: Db, 15 | ) {} 16 | 17 | /** 18 | * Creates a new unregistered user in the database 19 | * @param payload {CreateInviteUser} 20 | * @returns {Promise} result of the insert operation 21 | */ 22 | async create(payload: CreateInviteUser): Promise { 23 | const { email, teamIds } = payload; 24 | const UserInvites = { 25 | email, 26 | teamIds, 27 | createdAt: new Date(), 28 | }; 29 | const result = await this.db 30 | .collection(Collections.USERINVITES) 31 | .insertOne(UserInvites); 32 | return result; 33 | } 34 | 35 | /** 36 | * Updates a invite-user's teamIds by pushing all provided teamIds 37 | * @param payload {UpdateInviteUser} 38 | * @returns {Promise} 39 | */ 40 | async update(payload: UpdateInviteUser): Promise { 41 | const { email, teamIds } = payload; 42 | if (teamIds.length === 0) { 43 | const reponse = await this.removeByEmail(email); 44 | return reponse; 45 | } 46 | const result = await this.db 47 | .collection(Collections.USERINVITES) 48 | .updateOne({ email }, { $set: { teamIds: teamIds } }); 49 | return result; 50 | } 51 | 52 | /** 53 | * Retrieves a invite-user by their email 54 | * @param email {string} 55 | * @returns {Promise} 56 | */ 57 | async getByEmail(email: string): Promise { 58 | return this.db 59 | .collection(Collections.USERINVITES) 60 | .findOne({ email }); 61 | } 62 | 63 | /** 64 | * Removes a user invite completely by email 65 | * @param email {string} 66 | * @returns {Promise} 67 | */ 68 | async removeByEmail(email: string): Promise { 69 | const result = await this.db 70 | .collection(Collections.USERINVITES) 71 | .deleteOne({ email }); 72 | return result; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/modules/identity/strategies/google.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { ConfigService } from "@nestjs/config"; 3 | import { PassportStrategy } from "@nestjs/passport"; 4 | import { Strategy, VerifyCallback } from "passport-google-oauth20"; 5 | 6 | @Injectable() 7 | export class GoogleStrategy extends PassportStrategy(Strategy, "google") { 8 | constructor(private readonly configService: ConfigService) { 9 | const googleClientId = configService.get("oauth.google.clientId"); 10 | const googleClientSecret = configService.get("oauth.google.clientSecret"); 11 | const googleAppUrl = configService.get("oauth.google.appUrl"); 12 | const callbackUrl = `${googleAppUrl}/api/auth/google/callback`; 13 | 14 | super({ 15 | clientID: googleClientId, 16 | clientSecret: googleClientSecret, 17 | callbackURL: callbackUrl, 18 | scope: ["email", "profile"], 19 | }); 20 | } 21 | 22 | authorizationParams() { 23 | return { 24 | prompt: "consent", 25 | accessType: "offline", 26 | }; 27 | } 28 | 29 | async validate( 30 | accessToken: string, 31 | refreshToken: string, 32 | profile: any, 33 | done: VerifyCallback, 34 | ): Promise { 35 | const { id, emails, displayName } = profile; 36 | const user = { 37 | oAuthId: id, 38 | name: displayName, 39 | email: emails[0].value, 40 | }; 41 | done(null, user); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/modules/identity/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from "passport-jwt"; 2 | import { PassportStrategy } from "@nestjs/passport"; 3 | import { Inject, Injectable, UnauthorizedException } from "@nestjs/common"; 4 | import { ConfigService } from "@nestjs/config"; 5 | import { Db, ObjectId } from "mongodb"; 6 | import { JwtPayload } from "../payloads/jwt.payload"; 7 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 8 | import { ContextService } from "@src/modules/common/services/context.service"; 9 | import { ErrorMessages } from "@src/modules/common/enum/error-messages.enum"; 10 | 11 | /** 12 | * Jwt Strategy Class 13 | */ 14 | @Injectable() 15 | export class JwtStrategy extends PassportStrategy(Strategy) { 16 | /** 17 | * Constructor 18 | * @param {ConfigService} configService 19 | * @param {Db} mongodb 20 | */ 21 | constructor( 22 | readonly configService: ConfigService, 23 | @Inject("DATABASE_CONNECTION") 24 | private db: Db, 25 | private contextService: ContextService, 26 | ) { 27 | super({ 28 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 29 | ignoreExpiration: false, 30 | secretOrKey: configService.get("app.jwtSecretKey"), 31 | }); 32 | } 33 | 34 | /** 35 | * Checks if the bearer token is a valid token 36 | * @param {JwtPayload} jwtPayload validation method for jwt token 37 | * @param {any} done callback to resolve the request user with 38 | * @returns {Promise} whether or not to validate the jwt token 39 | */ 40 | async validate({ exp, _id, role }: JwtPayload) { 41 | const timeDiff = exp - Date.now() / 1000; 42 | if (timeDiff <= 0) { 43 | throw new UnauthorizedException(ErrorMessages.ExpiredToken); 44 | } 45 | const user = await this.db.collection(Collections.USER).findOne( 46 | { 47 | _id: new ObjectId(_id), 48 | }, 49 | { projection: { password: 0, refresh_tokens: 0, verificationCode: 0 } }, 50 | ); 51 | 52 | if (!user) { 53 | throw new UnauthorizedException(ErrorMessages.JWTFailed); 54 | } 55 | this.contextService.set("user", user); 56 | 57 | // if admin role exists we return the whole obj (this is only used for admin api's) 58 | if (role) { 59 | return { 60 | _id: user._id, 61 | email: user.email, 62 | name: user.name, 63 | role: role, 64 | }; 65 | } 66 | 67 | return user._id; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/modules/identity/strategies/refresh-token.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from "@nestjs/passport"; 2 | import { ExtractJwt, Strategy } from "passport-jwt"; 3 | import { FastifyRequest } from "fastify"; 4 | import { Inject, Injectable, UnauthorizedException } from "@nestjs/common"; 5 | import { ConfigService } from "@nestjs/config"; 6 | import { ErrorMessages } from "@src/modules/common/enum/error-messages.enum"; 7 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 8 | import { Db, ObjectId } from "mongodb"; 9 | import { ContextService } from "@src/modules/common/services/context.service"; 10 | 11 | @Injectable() 12 | export class RefreshTokenStrategy extends PassportStrategy( 13 | Strategy, 14 | "jwt-refresh", 15 | ) { 16 | constructor( 17 | readonly configService: ConfigService, 18 | @Inject("DATABASE_CONNECTION") 19 | private db: Db, 20 | private contextService: ContextService, 21 | ) { 22 | super({ 23 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 24 | secretOrKey: configService.get("app.refreshTokenSecretKey"), 25 | passReqToCallback: true, 26 | }); 27 | } 28 | 29 | async validate(req: FastifyRequest, payload: any) { 30 | const { _id, exp } = payload; 31 | const refreshToken = req.headers.authorization.replace("Bearer", "").trim(); 32 | const timeDiff = exp - Date.now() / 1000; 33 | if (timeDiff <= 0) { 34 | throw new UnauthorizedException(ErrorMessages.ExpiredToken); 35 | } 36 | const user = await this.db.collection(Collections.USER).findOne( 37 | { 38 | _id: new ObjectId(_id), 39 | }, 40 | { projection: { password: 0, refresh_tokens: 0, verificationCode: 0 } }, 41 | ); 42 | 43 | if (!user) { 44 | throw new UnauthorizedException(ErrorMessages.JWTFailed); 45 | } 46 | this.contextService.set("user", user); 47 | 48 | return { _id: payload._id, refreshToken }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/modules/proxy/controllers/http.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | Body, 5 | Req, 6 | Res, 7 | HttpException, 8 | HttpStatus, 9 | } from "@nestjs/common"; 10 | import { FastifyRequest, FastifyReply } from "fastify"; 11 | import { ApiBearerAuth, ApiBody, ApiOperation, ApiTags } from "@nestjs/swagger"; 12 | import { HttpService } from "../services/http.service"; 13 | 14 | @ApiBearerAuth() 15 | @ApiTags("proxy") 16 | @Controller("api/proxy") 17 | export class HttpController { 18 | constructor(private readonly httpService: HttpService) {} 19 | 20 | @Post("http-request") 21 | @ApiBody({ 22 | schema: { 23 | type: "object", 24 | properties: { 25 | url: { 26 | type: "string", 27 | description: "The URL to which the request will be sent", 28 | example: "https://api.example.com/resource", 29 | }, 30 | method: { 31 | type: "string", 32 | description: "HTTP method to be used (GET, POST, etc.)", 33 | example: "POST", 34 | }, 35 | headers: { 36 | type: "string", 37 | description: "JSON string representing an array of header objects", 38 | example: `[ 39 | { "key": "Authorization", "value": "Bearer your-token-here", "checked": true }, 40 | { "key": "Content-Type", "value": "application/json", "checked": true } 41 | ]`, 42 | }, 43 | body: { 44 | type: "string", 45 | description: "Request payload, format varies by contentType", 46 | example: `"{ \"key1\": \"value1\", \"key2\": \"value2\" }"`, 47 | }, 48 | contentType: { 49 | type: "string", 50 | description: "The content type of the request body", 51 | example: "application/json", 52 | }, 53 | }, 54 | }, 55 | }) 56 | @ApiOperation({ 57 | summary: "Proxy to route HTTP requests", 58 | description: `This will help address CORS error encountered when requests are made directly from a browser agent. 59 | Instead of browser agent, request will be routed through this proxy.`, 60 | }) 61 | async handleHttpRequest( 62 | @Body("url") url: string, 63 | @Body("method") method: string, 64 | @Body("headers") headers: string, 65 | @Body("body") body: any, 66 | @Body("contentType") contentType: string, 67 | @Req() req: FastifyRequest, 68 | @Res() res: FastifyReply, 69 | ) { 70 | try { 71 | const response = await this.httpService.makeHttpRequest({ 72 | url, 73 | method, 74 | headers, 75 | body, 76 | contentType, 77 | }); 78 | 79 | return res.status(HttpStatus.OK).send(response); 80 | } catch (error) { 81 | throw new HttpException(error.message, HttpStatus.BAD_GATEWAY); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/modules/proxy/gateway/socketio.gateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebSocketGateway, 3 | WebSocketServer, 4 | OnGatewayConnection, 5 | } from "@nestjs/websockets"; 6 | import { Server, Socket as SocketServer } from "socket.io"; 7 | import { SocketIoService } from "../services/socketio.service"; 8 | 9 | export const SOCKET_IO_PORT = 9001; 10 | 11 | @WebSocketGateway(SOCKET_IO_PORT, { 12 | cors: { 13 | origin: "*", 14 | methods: ["GET", "POST"], 15 | }, 16 | path: "/socket.io", 17 | transports: ["websocket"], 18 | }) 19 | export class SocketIoGateway implements OnGatewayConnection { 20 | @WebSocketServer() 21 | server: Server; 22 | 23 | constructor(private readonly socketIoService: SocketIoService) {} 24 | 25 | async afterInit() { 26 | console.log("Socket.io Handler Gateway initialized!"); 27 | } 28 | 29 | async handleConnection(proxySocketIO: SocketServer) { 30 | const { targetUrl, namespace, headers } = proxySocketIO.handshake.query; 31 | 32 | if (!targetUrl || !namespace) { 33 | proxySocketIO.disconnect(); 34 | console.error( 35 | "Missing required query parameters: url, namespace, or tabid", 36 | ); 37 | return; 38 | } 39 | 40 | try { 41 | const parsedHeaders = JSON.parse(headers as string); 42 | const headersObject: { [key: string]: string } = parsedHeaders.reduce( 43 | ( 44 | acc: Record, 45 | { key, value }: { key: string; value: string }, 46 | ) => { 47 | acc[key] = value; 48 | return acc; 49 | }, 50 | {} as { [key: string]: string }, 51 | ); 52 | 53 | // Establish a connection to the real Socket.IO server 54 | const targetSocketIO = await this.socketIoService.connectToTargetSocketIO( 55 | proxySocketIO, 56 | targetUrl as string, 57 | namespace as string, 58 | headersObject, 59 | ); 60 | 61 | proxySocketIO.on("disconnect", async () => { 62 | // Disconnecting target Socket.IO will automatically disconnects proxy Socket.IO in chain. 63 | targetSocketIO?.disconnect(); 64 | }); 65 | 66 | // Listen for all dynamic events from the frontend and forward them to target Socket.IO. 67 | proxySocketIO.onAny(async (event: string, args: any) => { 68 | try { 69 | if (event === "sparrow_internal_disconnect") { 70 | // Disconnecting target Socket.IO will automatically disconnects proxy Socket.IO in chain. 71 | targetSocketIO?.disconnect(); 72 | } else { 73 | targetSocketIO?.emit(event, args); 74 | } 75 | } catch (err) { 76 | console.error(`Failed to forward event ${event} for ${err.message}`); 77 | } 78 | }); 79 | } catch (err) { 80 | console.error( 81 | `Failed to connect to real Socket.IO server for ${err.message}`, 82 | ); 83 | proxySocketIO.disconnect(); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/modules/proxy/gateway/websocket.gateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebSocketGateway as WSGateway, 3 | WebSocketServer, 4 | OnGatewayConnection, 5 | OnGatewayDisconnect, 6 | } from "@nestjs/websockets"; 7 | import { WebSocketServer as WsServer, WebSocket } from "ws"; 8 | import * as url from "url"; 9 | import { 10 | WebsocketEvent, 11 | WebSocketService, 12 | } from "../services/websocket.service"; 13 | 14 | // Extend WebSocket to include custom properties 15 | interface CustomWebSocket extends WebSocket { 16 | query?: Record; 17 | } 18 | 19 | // @WSGateway({ path: "/ws", cors: true }) 20 | export class WebSocketGateway 21 | implements OnGatewayConnection, OnGatewayDisconnect 22 | { 23 | @WebSocketServer() 24 | server: WsServer; 25 | 26 | constructor(private readonly websocketService: WebSocketService) {} 27 | 28 | /** 29 | * Lifecycle hook that runs when the WebSocket gateway is initialized. 30 | */ 31 | async afterInit() { 32 | console.log("WebSocket Gateway initialized!"); 33 | } 34 | 35 | /** 36 | * Handle WebSocket connection from the frontend client. 37 | */ 38 | async handleConnection(client: CustomWebSocket, req: Request) { 39 | // Parse the URL from the upgrade request and attach it to the client 40 | const headers = req.headers; 41 | const parsedUrl = url.parse(req.url || "", true); 42 | client["query"] = parsedUrl.query; 43 | 44 | const tabid = client["query"].tabid as string; 45 | const targetUrl = client["query"].targetUrl as string; 46 | 47 | if (!tabid || !targetUrl) { 48 | console.error("Missing tabid or targetUrl in WebSocket URL."); 49 | client.close(4000, "TabID and TargetURL are required"); 50 | return; 51 | } 52 | 53 | console.log( 54 | `Received WebSocket connection: TabID=${tabid}, TargetURL=${targetUrl}`, 55 | ); 56 | 57 | // Establish the connection 58 | const success = await this.websocketService.establishConnection( 59 | client, 60 | tabid, 61 | targetUrl, 62 | headers, 63 | ); 64 | 65 | if (!success) { 66 | console.error( 67 | `Failed to connect to real WebSocket server for TabID=${tabid}`, 68 | ); 69 | client.close(4001, "Failed to connect to real WebSocket server"); 70 | } 71 | 72 | this.websocketService.sendEventToFrontendClient( 73 | tabid, 74 | WebsocketEvent.connect, 75 | ); 76 | 77 | client.on("message", (data) => { 78 | console.log("Frontend WebSocket client message received."); 79 | console.log(data); 80 | }); 81 | 82 | client.on("error", (error) => { 83 | console.error("Frontend WebSocket error:", error.message); 84 | this.websocketService.cleanupConnectionsByClient(client); 85 | }); 86 | } 87 | 88 | /** 89 | * Handle WebSocket disconnection from the frontend client. 90 | */ 91 | async handleDisconnect(client: WebSocket) { 92 | console.log("Frontend WebSocket client disconnected."); 93 | this.websocketService.cleanupConnectionsByClient(client); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/modules/proxy/proxy.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { HttpController } from "./controllers/http.controller"; 3 | import { HttpService } from "./services/http.service"; 4 | import { SocketIoService } from "./services/socketio.service"; 5 | import { SocketIoGateway } from "./gateway/socketio.gateway"; 6 | import { WebSocketGateway } from "./gateway/websocket.gateway"; 7 | import { WebSocketService } from "./services/websocket.service"; 8 | import { HttpModule } from "@nestjs/axios"; 9 | 10 | @Module({ 11 | imports: [HttpModule], 12 | controllers: [HttpController], 13 | providers: [ 14 | HttpService, 15 | SocketIoGateway, 16 | SocketIoService, 17 | WebSocketGateway, 18 | WebSocketService, 19 | ], 20 | exports: [SocketIoService, WebSocketService], 21 | }) 22 | export class ProxyModule {} 23 | -------------------------------------------------------------------------------- /src/modules/proxy/services/socketio.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { io, Socket as SocketClient } from "socket.io-client"; 3 | import { Socket as SocketServer } from "socket.io"; 4 | 5 | @Injectable() 6 | export class SocketIoService { 7 | /** 8 | * Makes a connection to the target Socket.IO server. 9 | */ 10 | async connectToTargetSocketIO( 11 | _proxySocketIO: SocketServer, 12 | _targetUrl: string, 13 | _namespace: string, 14 | _headers: Record = {}, 15 | ): Promise { 16 | const _targetSocketIO = io(`${_targetUrl}${_namespace}`, { 17 | transports: ["websocket"], 18 | extraHeaders: _headers, 19 | reconnection: false, 20 | }); 21 | 22 | // Listen for connect events from the target Socket.IO and forward to the proxy Socket.IO. 23 | _targetSocketIO.on("connect", () => { 24 | _proxySocketIO?.emit("sparrow_internal_connect", "io server connect"); 25 | }); 26 | 27 | // Listen for connect error events from the target Socket.IO and forward to the proxy Socket.IO. 28 | _targetSocketIO.on("connect_error", (err) => { 29 | _proxySocketIO?.emit("sparrow_internal_connect_error", err.message); 30 | _proxySocketIO?.disconnect(); 31 | }); 32 | 33 | // Listen for disconnect events from the target Socket.IO and forward to the proxy Socket.IO. 34 | _targetSocketIO.on("disconnect", (err) => { 35 | _proxySocketIO?.emit("sparrow_internal_disconnect", err); 36 | _proxySocketIO?.disconnect(); 37 | }); 38 | 39 | // Listen for all dynamic events from the target Socket.IO and forward to the proxy Socket.IO. 40 | _targetSocketIO.onAny((event: string, ...args: any[]) => { 41 | _proxySocketIO?.emit(event, args); 42 | }); 43 | 44 | return _targetSocketIO; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/user-admin/payloads/auth.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { IsNotEmpty, IsString } from "class-validator"; 3 | 4 | export class RefreshTokenDto { 5 | @ApiProperty({ 6 | description: "Refresh token", 7 | example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", 8 | }) 9 | @IsNotEmpty() 10 | @IsString() 11 | refreshToken: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/user-admin/payloads/members.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | 3 | export class HubMembersQuerySwaggerDto { 4 | @ApiProperty({ 5 | description: "Hub ID", 6 | required: true, 7 | }) 8 | hubId: string; 9 | 10 | @ApiProperty({ 11 | description: "Page number (starts at 1)", 12 | required: false, 13 | default: "1", 14 | }) 15 | page: string; 16 | 17 | @ApiProperty({ 18 | description: "Number of items per page", 19 | required: false, 20 | default: "10", 21 | }) 22 | limit: string; 23 | 24 | @ApiProperty({ 25 | description: "Search term to filter members by name or email", 26 | required: false, 27 | }) 28 | search: string; 29 | } 30 | 31 | export class HubInvitesQuerySwaggerDto { 32 | @ApiProperty({ 33 | description: "Hub ID", 34 | required: true, 35 | }) 36 | hubId: string; 37 | 38 | @ApiProperty({ 39 | description: "Page number (starts at 1)", 40 | required: false, 41 | default: "1", 42 | }) 43 | page: string; 44 | 45 | @ApiProperty({ 46 | description: "Number of items per page", 47 | required: false, 48 | default: "10", 49 | }) 50 | limit: string; 51 | 52 | @ApiProperty({ 53 | description: "Search term to filter invites by email", 54 | required: false, 55 | }) 56 | search: string; 57 | } 58 | -------------------------------------------------------------------------------- /src/modules/user-admin/payloads/workspace.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiPropertyOptional } from "@nestjs/swagger"; 2 | 3 | export class HubWorkspaceQuerySwaggerDto { 4 | @ApiPropertyOptional({ required: true }) 5 | hubId: string; 6 | 7 | @ApiPropertyOptional({ example: "1" }) 8 | page?: string; 9 | 10 | @ApiPropertyOptional({ example: "10" }) 11 | limit?: string; 12 | 13 | @ApiPropertyOptional() 14 | search?: string; 15 | 16 | @ApiPropertyOptional({ 17 | enum: ["name", "workspaceType", "createdAt", "updatedAt"], 18 | }) 19 | sortBy?: "name" | "workspaceType" | "createdAt" | "updatedAt"; 20 | 21 | @ApiPropertyOptional({ enum: ["asc", "desc"] }) 22 | sortOrder?: "asc" | "desc"; 23 | 24 | @ApiPropertyOptional({ 25 | enum: ["PRIVATE", "PUBLIC"], 26 | description: 27 | "Filter workspaces by visibility type. Leave empty to include all.", 28 | }) 29 | workspaceType?: "PRIVATE" | "PUBLIC"; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/user-admin/repositories/user-admin.auth.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from "@nestjs/common"; 2 | import { Db, ObjectId } from "mongodb"; 3 | import { Collections } from "src/modules/common/enum/database.collection.enum"; 4 | 5 | @Injectable() 6 | export class AdminAuthRepository { 7 | constructor(@Inject("DATABASE_CONNECTION") private readonly db: Db) {} 8 | 9 | async addRefreshTokenInUser( 10 | _id: ObjectId, 11 | refreshToken: string, 12 | ): Promise { 13 | await this.db.collection(Collections.USER).findOneAndUpdate( 14 | { _id }, 15 | { 16 | $set: { 17 | admin_refresh_tokens: [refreshToken], 18 | }, 19 | }, 20 | ); 21 | } 22 | 23 | async verifyRefreshToken( 24 | userId: ObjectId, 25 | hashedToken: string, 26 | ): Promise { 27 | const user = await this.db.collection(Collections.USER).findOne({ 28 | _id: userId, 29 | admin_refresh_tokens: hashedToken, 30 | }); 31 | 32 | return !!user; 33 | } 34 | 35 | /** 36 | * Update refresh token (replace old with new) 37 | */ 38 | async updateRefreshToken( 39 | userId: ObjectId, 40 | oldToken: string, 41 | newToken: string, 42 | ): Promise { 43 | return this.db.collection(Collections.USER).updateOne( 44 | { _id: userId, admin_refresh_tokens: oldToken }, 45 | { 46 | $set: { 47 | "admin_refresh_tokens.$": newToken, 48 | }, 49 | }, 50 | ); 51 | } 52 | 53 | async findUserById(_id: ObjectId): Promise { 54 | const user = await this.db.collection(Collections.USER).findOne({ _id }); 55 | if (!user) { 56 | throw new Error("User not found"); 57 | } 58 | return user; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/modules/user-admin/repositories/user-admin.members.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from "@nestjs/common"; 2 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 3 | import { Db, ObjectId } from "mongodb"; 4 | 5 | @Injectable() 6 | export class AdminMembersRepository { 7 | constructor(@Inject("DATABASE_CONNECTION") private db: Db) {} 8 | 9 | /** 10 | * Count workspaces a user has access to within a specific hub 11 | */ 12 | async countUserWorkspacesByHubId( 13 | memberId: string, 14 | hubId: string, 15 | ): Promise { 16 | try { 17 | const memberIdObj = 18 | typeof memberId === "string" ? new ObjectId(memberId) : memberId; 19 | 20 | // Find the user in the users collection 21 | const user = await this.db 22 | .collection(Collections.USER) 23 | .findOne({ _id: memberIdObj }, { projection: { workspaces: 1 } }); 24 | 25 | if (!user || !user.workspaces) { 26 | return 0; 27 | } 28 | 29 | // Count workspaces that belong to the specified hub 30 | const hubWorkspaces = user.workspaces.filter( 31 | (workspace: any) => 32 | workspace.teamId && workspace.teamId.toString() === hubId, 33 | ); 34 | 35 | return hubWorkspaces.length; 36 | } catch (error) { 37 | console.error( 38 | `Error getting workspace access for user ${memberId}:`, 39 | error, 40 | ); 41 | return 0; 42 | } 43 | } 44 | 45 | /** 46 | * Find users by ID array 47 | */ 48 | async findUsersByIds(userIds: ObjectId[]): Promise { 49 | try { 50 | return await this.db 51 | .collection(Collections.USER) 52 | .find({ _id: { $in: userIds } }) 53 | .project({ _id: 1, name: 1, email: 1 }) 54 | .toArray(); 55 | } catch (error) { 56 | console.error("Error finding users by IDs:", error); 57 | throw error; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/modules/user-admin/repositories/user-admin.updates.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from "@nestjs/common"; 2 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 3 | import { Db, ObjectId } from "mongodb"; 4 | 5 | @Injectable() 6 | export class AdminUpdatesRepository { 7 | constructor(@Inject("DATABASE_CONNECTION") private db: Db) {} 8 | 9 | /** 10 | * Get activities for users in the admin's teams - simple database query 11 | */ 12 | async findUpdates( 13 | query: any, 14 | page: number, 15 | limit: number, 16 | ): Promise<{ updates: any[]; total: number }> { 17 | try { 18 | const skip = (page - 1) * limit; 19 | 20 | // Convert string parameters to numbers to ensure they work correctly 21 | const numericLimit = Number(limit); 22 | const numericSkip = Number(skip); 23 | 24 | // Count total matching documents 25 | const total = await this.db 26 | .collection(Collections.UPDATES) 27 | .countDocuments(query); 28 | 29 | // Get updates with pagination 30 | const updates = await this.db 31 | .collection(Collections.UPDATES) 32 | .find(query) 33 | .sort({ createdAt: -1 }) 34 | .skip(numericSkip) 35 | .limit(numericLimit) 36 | .toArray(); 37 | 38 | return { updates, total }; 39 | } catch (error) { 40 | console.error("Error finding updates:", error); 41 | throw error; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/user-admin/user-admin.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | 3 | // ---- Module 4 | import { IdentityModule } from "../identity/identity.module"; 5 | 6 | import { AdminHubsController } from "./controllers/user-admin.hubs.controller"; 7 | import { AdminHubsRepository } from "./repositories/user-admin.hubs.repository"; 8 | import { AdminHubsService } from "./services/user-admin.hubs.service"; 9 | import { AdminWorkspaceRepository } from "./repositories/user-admin.workspace.repository"; 10 | import { AdminWorkspaceService } from "./services/user-admin.workspace.service"; 11 | import { AdminWorkspaceController } from "./controllers/user-admin.workspace.controller"; 12 | import { WorkspaceService } from "../workspace/services/workspace.service"; 13 | import { WorkspaceModule } from "../workspace/workspace.module"; 14 | import { AdminAuthController } from "./controllers/user-admin.auth.controller"; 15 | import { AdminAuthRepository } from "./repositories/user-admin.auth.repository"; 16 | import { AdminAuthService } from "./services/user-admin.auth.service"; 17 | import { JwtService } from "@nestjs/jwt"; 18 | import { TeamService } from "../identity/services/team.service"; 19 | import { AdminMembersService } from "./services/user-admin.members.service"; 20 | import { AdminMembersController } from "./controllers/user-admin.members.controller"; 21 | import { AdminMembersRepository } from "./repositories/user-admin.members.repository"; 22 | import { AdminUsersController } from "./controllers/user-admin.enterprise-user.controller"; 23 | import { AdminUsersService } from "./services/user-admin.enterprise-user.service"; 24 | import { AdminUpdatesRepository } from "./repositories/user-admin.updates.repository"; 25 | 26 | /** 27 | * Admin Module provides all necessary services, handlers, repositories, 28 | * and controllers related to the admin dashboard functionality. 29 | */ 30 | @Module({ 31 | imports: [IdentityModule, WorkspaceModule], 32 | providers: [ 33 | WorkspaceService, 34 | JwtService, 35 | AdminHubsService, 36 | AdminHubsRepository, 37 | AdminWorkspaceService, 38 | AdminWorkspaceRepository, 39 | AdminAuthRepository, 40 | AdminAuthService, 41 | TeamService, 42 | AdminMembersRepository, 43 | AdminMembersService, 44 | AdminUsersService, 45 | AdminUpdatesRepository, 46 | ], 47 | exports: [], 48 | controllers: [ 49 | AdminHubsController, 50 | AdminWorkspaceController, 51 | AdminAuthController, 52 | AdminMembersController, 53 | AdminUsersController, 54 | ], 55 | }) 56 | export class UserAdminModule {} 57 | -------------------------------------------------------------------------------- /src/modules/views/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{title}} 8 | 9 | 10 | 11 | 31 | 32 | 33 | 34 | 35 |
36 | {{{body}}} 37 |
38 | 39 | 40 | 41 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/modules/views/leaveTeamEmail.handlebars: -------------------------------------------------------------------------------- 1 | {{!< layoutName}} 2 | 3 | 4 | 5 | 6 | 7 | Leave Team 8 | 9 | 10 | 43 | 44 | 45 | 46 | 47 |
48 |
49 |
50 | 51 | {{> header}} 52 | 53 |
54 | 55 |
56 | 59 | 60 | 64 | 65 | 66 | 70 | 71 | 77 |
78 |
79 | 80 |
81 | 82 | {{> footer}} 83 | 84 | 85 |
86 |
87 |
88 | 89 | 90 | -------------------------------------------------------------------------------- /src/modules/views/magicCodeEmail.handlebars: -------------------------------------------------------------------------------- 1 | {{!< layoutName}} 2 | 3 | 4 | 5 | 6 | 7 | Sparrow Magic Code 8 | 9 | 10 | 73 | 74 | 75 | 76 | 77 |
78 |
79 |
80 | 81 | {{> header}} 82 | 83 |
84 | 85 |

Hi {{name}}

86 | 87 | 88 | 89 | 90 | 91 | 95 | 96 | 97 | 98 | 104 | 105 |
106 | 107 |
108 | 109 | 110 | {{> footer}} 111 | 112 | 113 |
114 |
115 |
116 | 117 | 118 | -------------------------------------------------------------------------------- /src/modules/views/newOwnerEmail.handlebars: -------------------------------------------------------------------------------- 1 | {{!< layoutName}} 2 | 3 | 4 | 5 | 6 | 7 | Leave Team 8 | 9 | 10 | 43 | 44 | 45 | 46 | 47 |
48 |
49 |
50 | 51 | {{> header}} 52 | 53 |
54 | 55 |
56 | 59 | 60 | 64 | 65 | 69 | 70 | 76 | 77 |
78 | 79 |
80 | 81 |
82 | 83 | 84 | {{> footer}} 85 | 86 | 87 |
88 |
89 |
90 | 91 | 92 | -------------------------------------------------------------------------------- /src/modules/views/newWorkspaceEmail.handlebars: -------------------------------------------------------------------------------- 1 | {{!< layoutName}} 2 | 3 | 4 | 5 | 6 | 7 | Leave Team 8 | 9 | 10 | 43 | 44 | 45 | 46 | 47 |
48 |
49 |
50 | 51 | {{> header}} 52 | 53 |
54 | 55 |
56 | 59 | 60 | 65 | 66 | 70 | 71 | 77 | 78 |
79 | 80 |
81 | 82 |
83 | 84 | 85 | {{> footer}} 86 | 87 | 88 |
89 |
90 |
91 | 92 | 93 | -------------------------------------------------------------------------------- /src/modules/views/oldOwnerEmail.handlebars: -------------------------------------------------------------------------------- 1 | {{!< layoutName}} 2 | 3 | 4 | 5 | 6 | 7 | Leave Team 8 | 9 | 10 | 43 | 44 | 45 | 46 | 47 |
48 |
49 |
50 | 51 | {{> header}} 52 | 53 |
54 | 55 |
56 | 59 | 60 | 65 | 66 | 72 | 73 | 79 | 80 |
81 | 82 |
83 | 84 |
85 | 86 | 87 | {{> footer}} 88 | 89 | 90 |
91 |
92 |
93 | 94 | 95 | -------------------------------------------------------------------------------- /src/modules/views/partials/footer.handlebars: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/views/partials/header.handlebars: -------------------------------------------------------------------------------- 1 |
2 | Sparrow Header 3 |
-------------------------------------------------------------------------------- /src/modules/workspace/controllers/ai-assistant.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post, Res, UseGuards } from "@nestjs/common"; 2 | import { AiAssistantService } from "../services/ai-assistant.service"; 3 | import { FastifyReply } from "fastify"; 4 | import { HttpStatusCode } from "@src/modules/common/enum/httpStatusCode.enum"; 5 | import { ApiResponseService } from "@src/modules/common/services/api-response.service"; 6 | import { 7 | ApiBearerAuth, 8 | ApiOperation, 9 | ApiResponse, 10 | ApiTags, 11 | } from "@nestjs/swagger"; 12 | import { JwtAuthGuard } from "@src/modules/common/guards/jwt-auth.guard"; 13 | import { 14 | PromptPayload, 15 | ErrorResponsePayload, 16 | ChatBotPayload 17 | } from "../payloads/ai-assistant.payload"; 18 | 19 | @ApiBearerAuth() 20 | @ApiTags("AI Support") 21 | @Controller("api/assistant") 22 | @UseGuards(JwtAuthGuard) 23 | export class AiAssistantController { 24 | /** 25 | * Constructor to initialize AiAssistantController with the required service. 26 | * @param aiAssistantService - Injected AiAssistantService to handle business logic. 27 | */ 28 | constructor(private readonly aiAssistantService: AiAssistantService) {} 29 | 30 | @ApiOperation({ 31 | summary: "Get a respose for AI assistant", 32 | description: "this will return AI response from the input prompt", 33 | }) 34 | @ApiResponse({ 35 | status: 201, 36 | description: "AI response Generated Successfully", 37 | }) 38 | @ApiResponse({ status: 400, description: "Generate AI Response Failed" }) 39 | @Post("prompt") 40 | async generate(@Body() prompt: PromptPayload, @Res() res: FastifyReply) { 41 | const data = await this.aiAssistantService.generateText(prompt); 42 | const response = new ApiResponseService( 43 | "AI Reposonse Generated", 44 | HttpStatusCode.CREATED, 45 | data, 46 | ); 47 | return res.status(response.httpStatusCode).send(response); 48 | } 49 | 50 | @Post("specific-error") 51 | async CurlError( 52 | @Body() errorResponse: ErrorResponsePayload, 53 | @Res() res: FastifyReply, 54 | ) { 55 | const data = await this.aiAssistantService.specificError(errorResponse); 56 | const response = new ApiResponseService( 57 | "AI Error Handler Reposonse Generated", 58 | HttpStatusCode.CREATED, 59 | data, 60 | ); 61 | return res.status(response.httpStatusCode).send(response); 62 | } 63 | 64 | @Post("generate-prompt") 65 | async GeneratePrompt( 66 | @Body() payload: ChatBotPayload, 67 | @Res() res: FastifyReply, 68 | ) { 69 | const data = await this.aiAssistantService.promptGeneration(payload); 70 | const response = new ApiResponseService( 71 | "Prompt Generated", 72 | HttpStatusCode.CREATED, 73 | data, 74 | ); 75 | return res.status(response.httpStatusCode).send(response); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/modules/workspace/controllers/ai-assistant.gateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebSocketGateway, 3 | WebSocketServer, 4 | OnGatewayConnection, 5 | OnGatewayDisconnect, 6 | OnGatewayInit, 7 | } from "@nestjs/websockets"; 8 | import { Server, WebSocket } from "ws"; 9 | import { AiAssistantService } from "../services/ai-assistant.service"; 10 | 11 | /** 12 | * WebSocket Gateway for AI Assistant. 13 | * Handles WebSocket connections, disconnections, and incoming messages 14 | * for the AI Assistant service. 15 | */ 16 | 17 | @WebSocketGateway({ path: "/ai-assistant" , cors: true}) 18 | export class AiAssistantGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit { 19 | 20 | @WebSocketServer() 21 | private server: Server; 22 | 23 | constructor(private readonly aiAssistantService: AiAssistantService) {} 24 | 25 | afterInit(server: Server) { 26 | console.log("WebSocket server initialized"); 27 | } 28 | 29 | async handleConnection(client: WebSocket) { 30 | console.log("Client connected"); 31 | 32 | client.on("close", () => { 33 | console.log("Client disconnected"); 34 | }); 35 | 36 | if (client.readyState === WebSocket.OPEN) { 37 | client.send(JSON.stringify({ event: "connected", message: "Welcome to AI Assistant!" })); 38 | this.aiAssistantService.generateTextChatBot(client); 39 | } 40 | } 41 | 42 | async handleDisconnect(client: WebSocket) { 43 | console.log("Client disconnected"); 44 | } 45 | } 46 | 47 | // @WebSocketGateway({ path: "/dummy" }) 48 | // export class DummyGateway 49 | // implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect 50 | // { 51 | // @WebSocketServer() 52 | // server: Server; 53 | 54 | // constructor() {} 55 | // async afterInit() { 56 | // console.log("AI Websocket Gateway initialized!"); 57 | // } 58 | 59 | // async handleConnection(client: Socket) { 60 | // setTimeout(() => { 61 | // client.emit("Client", "Client is connected, first event initiated."); 62 | // }, 5000); 63 | // } 64 | 65 | // handleDisconnect(client: Socket) { 66 | // console.log(`Client disconnected: ${client.id}`); 67 | // } 68 | 69 | // @SubscribeMessage("") 70 | // async handleMessage2( 71 | // @ConnectedSocket() client: Socket, 72 | // @MessageBody() payload: string, 73 | // ) { 74 | // client.emit("third", payload); 75 | // } 76 | 77 | // @SubscribeMessage("second") 78 | // async handleMessage3( 79 | // @ConnectedSocket() client: Socket, 80 | // @MessageBody() payload: string, 81 | // ) { 82 | // client.emit("second", payload); 83 | // client.emit("latest", payload); 84 | // } 85 | 86 | // @SubscribeMessage("first") 87 | // async handleMessage( 88 | // @ConnectedSocket() client: Socket, 89 | // @MessageBody() payload: string, 90 | // ) { 91 | // client.emit("first", payload); 92 | // client.emit("new", payload); 93 | // } 94 | // } -------------------------------------------------------------------------------- /src/modules/workspace/controllers/chatbot-stats.controller.ts: -------------------------------------------------------------------------------- 1 | // ---- NestJS Native imports 2 | import { Body, Controller, Post, Res, UseGuards } from "@nestjs/common"; 3 | 4 | // ---- Fastify 5 | import { FastifyReply } from "fastify"; 6 | 7 | // ---- Swagger 8 | import { 9 | ApiBearerAuth, 10 | ApiOperation, 11 | ApiResponse, 12 | ApiTags, 13 | } from "@nestjs/swagger"; 14 | 15 | // ---- Enums 16 | import { HttpStatusCode } from "@src/modules/common/enum/httpStatusCode.enum"; 17 | 18 | // ---- Services 19 | import { ApiResponseService } from "@src/modules/common/services/api-response.service"; 20 | import { ChatbotStatsService } from "../services/chatbot-stats.service"; 21 | 22 | // ---- Guard 23 | import { JwtAuthGuard } from "@src/modules/common/guards/jwt-auth.guard"; 24 | 25 | // ---- Payload 26 | import { ChatbotFeedbackDto } from "../payloads/chatbot-stats.payload"; 27 | 28 | /** 29 | * ChatbotStatsController handles API endpoints related to chatbot statistics and feedback. 30 | * It uses JWT authentication for security and integrates with Fastify for response handling. 31 | */ 32 | @ApiBearerAuth() 33 | @ApiTags("Chatbot Stats") 34 | @Controller("api/chatbotstats") 35 | @UseGuards(JwtAuthGuard) 36 | export class ChatbotStatsController { 37 | /** 38 | * Constructor to initialize ChatbotStatsController with the required service. 39 | * @param chatbotStatsController - Injected ChatbotStatsController to handle business logic. 40 | */ 41 | constructor(private readonly chatbotStatsService: ChatbotStatsService) {} 42 | 43 | /** 44 | * Endpoint to add or update feedback for AI-generated responses. 45 | * @param payload - The feedback data to be added or updated. 46 | * @param res - The Fastify reply object. 47 | * @returns A response with the status and message indicating the result of the operation. 48 | */ 49 | @ApiOperation({ 50 | summary: "Add a feedback for AI generated response", 51 | description: 52 | "This will update or add the feedback for the AI generated response", 53 | }) 54 | @ApiResponse({ 55 | status: 201, 56 | description: "Feedback Added Successfully", 57 | }) 58 | @ApiResponse({ status: 400, description: "Failed to add feedback" }) 59 | @Post("feedback") 60 | async updateFeedback( 61 | @Body() payload: ChatbotFeedbackDto, 62 | @Res() res: FastifyReply, 63 | ) { 64 | const data = await this.chatbotStatsService.updateFeedback(payload); 65 | const response = new ApiResponseService( 66 | "AI Feedack updates", 67 | HttpStatusCode.OK, 68 | data, 69 | ); 70 | return res.status(response.httpStatusCode).send(response); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/modules/workspace/controllers/mock-server.controller.ts: -------------------------------------------------------------------------------- 1 | import { All, Controller, Req, Res } from "@nestjs/common"; 2 | import { 3 | ApiBearerAuth, 4 | ApiOperation, 5 | ApiResponse, 6 | ApiTags, 7 | } from "@nestjs/swagger"; 8 | 9 | // ---- Fastify 10 | import { FastifyReply, FastifyRequest } from "fastify"; 11 | 12 | // ---- Enum 13 | import { HttpStatusCode } from "@src/modules/common/enum/httpStatusCode.enum"; 14 | import { MockServerService } from "../services/mock-server.service"; 15 | 16 | /** 17 | * Mock Server Controller 18 | */ 19 | @ApiBearerAuth() 20 | @ApiTags("mock-server") 21 | @Controller("api/mock") // Base route for this controller 22 | export class MockServerController { 23 | constructor(private readonly mockServerService: MockServerService) {} 24 | 25 | /** 26 | * Mock Server requests route, catches all the mock requests. 27 | */ 28 | @ApiOperation({ 29 | summary: "Get the mock server request", 30 | description: 31 | "All the mock server requests will come here and return saved response.", 32 | }) // Provides metadata for this operation in Swagger documentation 33 | @ApiResponse({ 34 | status: 201, 35 | description: "Received Mock request succcessfully", 36 | }) 37 | @ApiResponse({ status: 400, description: "Failed to receive mock request" }) 38 | @All("*") 39 | async getMockRequests(@Req() req: FastifyRequest, @Res() res: FastifyReply) { 40 | const response = await this.mockServerService.handleMockRequests(req); 41 | if (response?.contentType) { 42 | res.header("Content-Type", response.contentType); 43 | } 44 | return res.status(response.status).send(response.body); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/workspace/controllers/updates.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Res, UseGuards } from "@nestjs/common"; 2 | import { 3 | ApiBearerAuth, 4 | ApiOperation, 5 | ApiResponse, 6 | ApiTags, 7 | } from "@nestjs/swagger"; 8 | 9 | // ---- Fastify 10 | import { FastifyReply } from "fastify"; 11 | 12 | // ---- Guard 13 | import { JwtAuthGuard } from "@src/modules/common/guards/jwt-auth.guard"; 14 | 15 | // ---- Enum 16 | import { HttpStatusCode } from "@src/modules/common/enum/httpStatusCode.enum"; 17 | 18 | // ---- Services 19 | import { ApiResponseService } from "@src/modules/common/services/api-response.service"; 20 | import { UpdatesService } from "../services/updates.service"; 21 | 22 | /** 23 | * Updates Controller 24 | */ 25 | @ApiBearerAuth() 26 | @ApiTags("updates") 27 | @Controller("api/updates") // Base route for this controller 28 | @UseGuards(JwtAuthGuard) // JWT authentication guard to protect routes 29 | export class UpdatesController { 30 | /** 31 | * Constructor to initialize UpdatesController with the required service. 32 | * @param updatesService - Injected UpdatesService to handle business logic. 33 | */ 34 | constructor(private readonly updatesServie: UpdatesService) {} 35 | 36 | /** 37 | * Fetches updates for a specific workspace in batches of 20. 38 | * 39 | * @param workspaceId - The ID of the workspace to fetch updates for. 40 | * @param pageNumber - The page number of the updates batch to retrieve. 41 | * @param res - Fastify reply object for sending the response. 42 | * @returns A Fastify response with the updates or an error message. 43 | */ 44 | @Get(":workspaceId/page/:pageNumber") 45 | @ApiOperation({ 46 | summary: "Get the Updates", 47 | description: "You can fetch specific workspace updates on a batch of 20", 48 | }) // Provides metadata for this operation in Swagger documentation 49 | @ApiResponse({ 50 | status: 201, 51 | description: "Workspace updates received succcessfully", 52 | }) 53 | @ApiResponse({ status: 400, description: "Failed to retrieve updates" }) 54 | async getUpdates( 55 | @Param("workspaceId") workspaceId: string, 56 | @Param("pageNumber") pageNumber: string, 57 | @Res() res: FastifyReply, 58 | ) { 59 | const updates = await this.updatesServie.findUpdatesByWorkspace( 60 | workspaceId, 61 | pageNumber, 62 | ); // Calls the updates service to get the updates 63 | const responseData = new ApiResponseService( 64 | "Updates received", 65 | HttpStatusCode.CREATED, 66 | updates, 67 | ); 68 | return res.status(responseData.httpStatusCode).send(responseData); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/workspace/handlers/addUser.handler.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable, OnModuleInit } from "@nestjs/common"; 2 | import { TOPIC } from "@src/modules/common/enum/topic.enum"; 3 | import { SUBSCRIPTION } from "@src/modules/common/enum/subscription.enum"; 4 | import { WorkspaceUserService } from "../services/workspace-user.service"; 5 | import { ConsumerService } from "@src/modules/common/services/kafka/consumer.service"; 6 | 7 | @Injectable() 8 | export class AddUserHandler implements OnModuleInit { 9 | constructor( 10 | private readonly workspaceUserService: WorkspaceUserService, 11 | private readonly consumerService: ConsumerService, 12 | ) {} 13 | 14 | async onModuleInit() { 15 | await this.consumerService.consume({ 16 | topic: { topic: TOPIC.USER_ADDED_TO_TEAM_TOPIC }, 17 | config: { groupId: SUBSCRIPTION.USER_ADDED_TO_TEAM_SUBSCRIPTION }, 18 | onMessage: async (message) => { 19 | const data = JSON.parse(message.value.toString()); 20 | const workspaceArray = data.teamWorkspaces; 21 | const userId = data.userId; 22 | const role = data.role; 23 | await this.workspaceUserService.addUserInWorkspace( 24 | workspaceArray, 25 | userId, 26 | role, 27 | ); 28 | }, 29 | onError: async (error) => { 30 | throw new BadRequestException(error); 31 | }, 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/workspace/handlers/ai-log.handler.ts: -------------------------------------------------------------------------------- 1 | // ---- NestJS Native Imports 2 | import { BadRequestException, Injectable, OnModuleInit } from "@nestjs/common"; 3 | 4 | // ---- Enums 5 | import { TOPIC } from "@src/modules/common/enum/topic.enum"; 6 | import { SUBSCRIPTION } from "@src/modules/common/enum/subscription.enum"; 7 | 8 | // ---- Services 9 | import { ConsumerService } from "@src/modules/common/services/kafka/consumer.service"; 10 | import { AiLogService } from "../services/ai-log.service"; 11 | 12 | @Injectable() 13 | export class AiLogHandler implements OnModuleInit { 14 | 15 | constructor( 16 | private readonly ailogService: AiLogService, 17 | private readonly consumerService: ConsumerService, 18 | ) {} 19 | 20 | /** 21 | * onModuleInit is called when the module is initialized. 22 | * It sets up a Kafka consumer to listen to the AI response generated topic and process messages. 23 | */ 24 | async onModuleInit() { 25 | await this.consumerService.consume({ 26 | topic: { topic: TOPIC.AI_ACTIVITY_LOG_TOPIC }, 27 | config: { groupId: SUBSCRIPTION.AI_LOGS_GENERATOR_SUBSCRIPTION }, 28 | onMessage: async (message) => { 29 | const data = JSON.parse(message.value.toString()); 30 | const userId = data.userId.toString(); 31 | const activity = data.activity.toString(); 32 | const model = data.model.toString(); 33 | const tokenConsumed = data.tokenConsumed; 34 | const thread_id = data.threadId.toString(); 35 | await this.ailogService.addLog({ userId, activity, model, tokenConsumed, thread_id }); 36 | }, 37 | onError: async (error) => { 38 | throw new BadRequestException(error); 39 | }, 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/modules/workspace/handlers/chatbot-token.handler.ts: -------------------------------------------------------------------------------- 1 | // ---- NestJS Native Imports 2 | import { BadRequestException, Injectable, OnModuleInit } from "@nestjs/common"; 3 | 4 | // ---- Enums 5 | import { TOPIC } from "@src/modules/common/enum/topic.enum"; 6 | import { SUBSCRIPTION } from "@src/modules/common/enum/subscription.enum"; 7 | 8 | // ---- Services 9 | import { ConsumerService } from "@src/modules/common/services/kafka/consumer.service"; 10 | import { ChatbotStatsService } from "../services/chatbot-stats.service"; 11 | 12 | /** 13 | * ChatbotTokenHandler class is responsible for handling token updates for chatbot responses. 14 | * It implements the OnModuleInit interface to initialize Kafka consumers on module initialization. 15 | * This handler consumes AI_RESPONSE_GENERATED_TOPIC. 16 | */ 17 | @Injectable() 18 | export class ChatbotTokenHandler implements OnModuleInit { 19 | /** 20 | * Constructor to initialize ChatbotTokenHandler with required services. 21 | * @param chatbotStatsService - Service to handle chatbot statistics updates. 22 | * @param consumerService - Kafka consumer service to consume messages from Kafka topics. 23 | */ 24 | constructor( 25 | private readonly chatbotStatsService: ChatbotStatsService, 26 | private readonly consumerService: ConsumerService, 27 | ) {} 28 | 29 | /** 30 | * onModuleInit is called when the module is initialized. 31 | * It sets up a Kafka consumer to listen to the AI response generated topic and process messages. 32 | */ 33 | async onModuleInit() { 34 | await this.consumerService.consume({ 35 | topic: { topic: TOPIC.AI_RESPONSE_GENERATED_TOPIC }, 36 | config: { groupId: SUBSCRIPTION.AI_RESPONSE_GENERATED_SUBSCRIPTION }, 37 | onMessage: async (message) => { 38 | const data = JSON.parse(message.value.toString()); 39 | const tokenCount = data.tokenCount; 40 | const userId = data.userId.toString(); 41 | const model = data.model.toString(); 42 | await this.chatbotStatsService.updateToken({ userId, tokenCount, model }); 43 | }, 44 | onError: async (error) => { 45 | throw new BadRequestException(error); 46 | }, 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/modules/workspace/handlers/demoteAdmin.handlers.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable, OnModuleInit } from "@nestjs/common"; 2 | import { TOPIC } from "@src/modules/common/enum/topic.enum"; 3 | import { SUBSCRIPTION } from "@src/modules/common/enum/subscription.enum"; 4 | import { WorkspaceUserService } from "../services/workspace-user.service"; 5 | import { ConsumerService } from "@src/modules/common/services/kafka/consumer.service"; 6 | 7 | @Injectable() 8 | export class DemoteAdminHandler implements OnModuleInit { 9 | constructor( 10 | private readonly workspaceUserService: WorkspaceUserService, 11 | private readonly consumerService: ConsumerService, 12 | ) {} 13 | 14 | async onModuleInit() { 15 | await this.consumerService.consume({ 16 | topic: { topic: TOPIC.TEAM_ADMIN_DEMOTED_TOPIC }, 17 | config: { groupId: SUBSCRIPTION.TEAM_ADMIN_DEMOTED_SUBSCRIPTION }, 18 | onMessage: async (message) => { 19 | const data = JSON.parse(message.value.toString()); 20 | const workspaceArray = data.teamWorkspaces; 21 | const userId = data.userId; 22 | await this.workspaceUserService.demoteAdminInWorkspace( 23 | workspaceArray, 24 | userId, 25 | ); 26 | }, 27 | onError: async (error) => { 28 | throw new BadRequestException(error); 29 | }, 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/workspace/handlers/promoteAdmin.handlers.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable, OnModuleInit } from "@nestjs/common"; 2 | import { TOPIC } from "@src/modules/common/enum/topic.enum"; 3 | import { SUBSCRIPTION } from "@src/modules/common/enum/subscription.enum"; 4 | import { WorkspaceUserService } from "../services/workspace-user.service"; 5 | import { ConsumerService } from "@src/modules/common/services/kafka/consumer.service"; 6 | 7 | @Injectable() 8 | export class PromoteAdminHandler implements OnModuleInit { 9 | constructor( 10 | private readonly workspaceUserService: WorkspaceUserService, 11 | private readonly consumerService: ConsumerService, 12 | ) {} 13 | 14 | async onModuleInit() { 15 | await this.consumerService.consume({ 16 | topic: { topic: TOPIC.TEAM_ADMIN_ADDED_TOPIC }, 17 | config: { groupId: SUBSCRIPTION.TEAM_ADMIN_ADDED_SUBSCRIPTION }, 18 | onMessage: async (message) => { 19 | const data = JSON.parse(message.value.toString()); 20 | const workspaceArray = data.teamWorkspaces; 21 | const userId = data.userId; 22 | await this.workspaceUserService.updateAdminRoleInWorkspace( 23 | workspaceArray, 24 | userId, 25 | ); 26 | }, 27 | onError: async (error) => { 28 | throw new BadRequestException(error); 29 | }, 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/workspace/handlers/removeUser.handler.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable, OnModuleInit } from "@nestjs/common"; 2 | import { TOPIC } from "@src/modules/common/enum/topic.enum"; 3 | import { SUBSCRIPTION } from "@src/modules/common/enum/subscription.enum"; 4 | import { WorkspaceUserService } from "../services/workspace-user.service"; 5 | import { ConsumerService } from "@src/modules/common/services/kafka/consumer.service"; 6 | 7 | @Injectable() 8 | export class RemoveUserHandler implements OnModuleInit { 9 | constructor( 10 | private readonly workspaceUserService: WorkspaceUserService, 11 | private readonly consumerService: ConsumerService, 12 | ) {} 13 | 14 | async onModuleInit() { 15 | await this.consumerService.consume({ 16 | topic: { topic: TOPIC.USER_REMOVED_FROM_TEAM_TOPIC }, 17 | config: { groupId: SUBSCRIPTION.USER_REMOVED_FROM_TEAM_SUBSCRIPTION }, 18 | onMessage: async (message) => { 19 | const data = JSON.parse(message.value.toString()); 20 | const workspaceArray = data.teamWorkspaces; 21 | const userId = data.userId; 22 | const role = data.role; 23 | await this.workspaceUserService.removeUserFromWorkspace( 24 | workspaceArray, 25 | userId, 26 | role, 27 | ); 28 | }, 29 | onError: async (error) => { 30 | throw new BadRequestException(error); 31 | }, 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/workspace/handlers/teamUpdated.handler.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable, OnModuleInit } from "@nestjs/common"; 2 | // ---- Enum 3 | import { TOPIC } from "@src/modules/common/enum/topic.enum"; 4 | import { SUBSCRIPTION } from "@src/modules/common/enum/subscription.enum"; 5 | // ---- Services 6 | import { ConsumerService } from "@src/modules/common/services/kafka/consumer.service"; 7 | import { WorkspaceService } from "../services/workspace.service"; 8 | 9 | @Injectable() 10 | export class TeamUpdatedHandler implements OnModuleInit { 11 | constructor( 12 | private readonly workspaceService: WorkspaceService, 13 | private readonly consumerService: ConsumerService, 14 | ) {} 15 | 16 | /** 17 | * Initializes the module by subscribing to the Kafka topic that listens for team details updates. 18 | * When a message is received, it updates the team details in all related workspaces. 19 | * 20 | * @throws {BadRequestException} If an error occurs during message consumption or processing. 21 | */ 22 | async onModuleInit() { 23 | // Set up the consumer to listen for messages from the TEAM_DETAILS_UPDATED_TOPIC Kafka topic 24 | await this.consumerService.consume({ 25 | topic: { topic: TOPIC.TEAM_DETAILS_UPDATED_TOPIC }, 26 | config: { groupId: SUBSCRIPTION.TEAM_DETAILS_UPDATED_SUBSCRIPTION }, 27 | // Callback function executed when a message is received 28 | onMessage: async (message) => { 29 | const data = JSON.parse(message.value.toString()); 30 | const teamId = data.teamId.toString(); 31 | const teamName = data.teamName; 32 | const teamWorkspaces = data.teamWorkspaces; 33 | 34 | // Update the team details in the workspaces 35 | await this.workspaceService.updateTeamDetailsInWorkspace( 36 | teamId, 37 | teamName, 38 | teamWorkspaces, 39 | ); 40 | }, 41 | 42 | // Callback function executed when an error occurs 43 | onError: async (error) => { 44 | // Throw a BadRequestException on error 45 | throw new BadRequestException(error); 46 | }, 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/modules/workspace/handlers/updates.handler.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable, OnModuleInit } from "@nestjs/common"; 2 | // ---- Enums 3 | import { TOPIC } from "@src/modules/common/enum/topic.enum"; 4 | import { SUBSCRIPTION } from "@src/modules/common/enum/subscription.enum"; 5 | 6 | // ---- Services 7 | import { ConsumerService } from "@src/modules/common/services/kafka/consumer.service"; 8 | import { UpdatesService } from "../services/updates.service"; 9 | /** 10 | * UpdatesHandler class handles Kafka messages related to updates. 11 | */ 12 | @Injectable() 13 | export class UpdatesHandler implements OnModuleInit { 14 | /** 15 | * Constructor to initialize UpdatesHandler with the required services. 16 | * @param updatesService - Injected UpdatesService to handle business logic. 17 | * @param consumerService - Injected ConsumerService to handle Kafka consumption. 18 | */ 19 | constructor( 20 | private readonly updatesService: UpdatesService, 21 | private readonly consumerService: ConsumerService, 22 | ) {} 23 | 24 | /** 25 | * Initializes the UpdatesHandler module and starts consuming Kafka messages. 26 | */ 27 | async onModuleInit() { 28 | await this.consumerService.consume({ 29 | topic: { topic: TOPIC.UPDATES_ADDED_TOPIC }, 30 | config: { groupId: SUBSCRIPTION.UPDATES_ADDED_SUBSCRIPTION }, 31 | onMessage: async (message) => { 32 | const data = JSON.parse(message.value.toString()); 33 | const type = data.type; 34 | const updateMessage = data.message; 35 | const workspaceId = data.workspaceId; 36 | await this.updatesService.addUpdate({ 37 | type, 38 | message: updateMessage, 39 | workspaceId, 40 | }); 41 | }, 42 | onError: async (error) => { 43 | throw new BadRequestException(error); 44 | }, 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/modules/workspace/payloads/ai-log.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { Type } from "class-transformer"; 3 | import { 4 | IsArray, 5 | IsBoolean, 6 | IsMongoId, 7 | IsNotEmpty, 8 | IsNumber, 9 | IsOptional, 10 | IsString, 11 | ValidateNested, 12 | IsDate 13 | } from "class-validator"; 14 | 15 | 16 | export class LogUpdateDTO { 17 | 18 | /** 19 | * The unique identifier of the user who interacted with the chatbot. 20 | * This field is required and must be a valid MongoDB ObjectId. 21 | */ 22 | @ApiProperty() 23 | @IsMongoId() 24 | @IsNotEmpty() 25 | @IsString() 26 | userId: string; 27 | 28 | 29 | @ApiProperty() 30 | @IsMongoId() 31 | @IsNotEmpty() 32 | @IsString() 33 | activity: string; 34 | 35 | 36 | @ApiProperty() 37 | @IsMongoId() 38 | @IsNotEmpty() 39 | @IsString() 40 | model: string; 41 | 42 | /** 43 | * The total number of tokens consumed during the chatbot interaction. 44 | * This field is required. 45 | */ 46 | @ApiProperty() 47 | @IsNumber() 48 | @IsNotEmpty() 49 | tokenConsumed: number; 50 | 51 | @ApiProperty() 52 | @IsMongoId() 53 | @IsNotEmpty() 54 | @IsString() 55 | thread_id: string; 56 | 57 | } 58 | 59 | /** 60 | * Data Transfer Object for updating response token. 61 | */ 62 | export class LogDTO { 63 | 64 | /** 65 | * The unique identifier of the user who interacted with the chatbot. 66 | * This field is required and must be a valid MongoDB ObjectId. 67 | */ 68 | @ApiProperty() 69 | @IsMongoId() 70 | @IsNotEmpty() 71 | @IsString() 72 | userId: string; 73 | 74 | 75 | @ApiProperty() 76 | @IsMongoId() 77 | @IsNotEmpty() 78 | @IsString() 79 | activity: string; 80 | 81 | 82 | @ApiProperty() 83 | @IsMongoId() 84 | @IsNotEmpty() 85 | @IsString() 86 | model: string; 87 | 88 | /** 89 | * The total number of tokens consumed during the chatbot interaction. 90 | * This field is required. 91 | */ 92 | @ApiProperty() 93 | @IsNumber() 94 | @IsNotEmpty() 95 | tokenConsumed: number; 96 | 97 | @ApiProperty() 98 | @IsMongoId() 99 | @IsNotEmpty() 100 | @IsString() 101 | thread_id: string; 102 | 103 | } -------------------------------------------------------------------------------- /src/modules/workspace/payloads/branch.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { CollectionItem } from "@src/modules/common/models/collection.model"; 3 | import { Type } from "class-transformer"; 4 | import { 5 | IsArray, 6 | IsDate, 7 | IsNotEmpty, 8 | IsOptional, 9 | IsString, 10 | ValidateNested, 11 | } from "class-validator"; 12 | 13 | export class createBranchDto { 14 | @IsString() 15 | @ApiProperty({ required: true, example: "Branch name" }) 16 | @IsNotEmpty() 17 | name: string; 18 | 19 | @IsString() 20 | @ApiProperty({ required: true, example: "65ed7a82af45cb59f471a983" }) 21 | @IsNotEmpty() 22 | collectionId: string; 23 | 24 | @ApiProperty({ type: [CollectionItem] }) 25 | @IsArray() 26 | @ValidateNested({ each: true }) 27 | @Type(() => CollectionItem) 28 | items: CollectionItem[]; 29 | } 30 | 31 | export class UpdateBranchDto { 32 | @ApiProperty({ type: [CollectionItem] }) 33 | @IsArray() 34 | @ValidateNested({ each: true }) 35 | @Type(() => CollectionItem) 36 | items: CollectionItem[]; 37 | 38 | @IsDate() 39 | @IsOptional() 40 | updatedAt?: Date; 41 | 42 | @IsString() 43 | @IsOptional() 44 | updatedBy?: string; 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/workspace/payloads/environment.payload.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsString, 3 | IsMongoId, 4 | IsNotEmpty, 5 | IsArray, 6 | ValidateNested, 7 | IsOptional, 8 | } from "class-validator"; 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Type } from "class-transformer"; 11 | import { VariableDto } from "@src/modules/common/models/environment.model"; 12 | 13 | export class CreateEnvironmentDto { 14 | @IsString() 15 | @ApiProperty({ required: true, example: "Environment name" }) 16 | @IsNotEmpty() 17 | name: string; 18 | 19 | @ApiProperty({ required: true, example: "6544cdea4b3d3b043a96c307" }) 20 | @IsMongoId() 21 | @IsNotEmpty() 22 | @IsOptional() 23 | workspaceId?: string; 24 | 25 | @ApiProperty({ 26 | required: true, 27 | example: [ 28 | { 29 | key: "key", 30 | value: "value", 31 | checked: true, 32 | }, 33 | ], 34 | }) 35 | @IsArray() 36 | @Type(() => VariableDto) 37 | @ValidateNested({ each: true }) 38 | variable: VariableDto[]; 39 | } 40 | 41 | export class UpdateEnvironmentDto { 42 | @IsString() 43 | @ApiProperty({ required: true, example: "New environment name" }) 44 | @IsNotEmpty() 45 | name: string; 46 | 47 | @ApiProperty({ 48 | required: false, 49 | example: [ 50 | { 51 | key: "key", 52 | value: "value", 53 | checked: true, 54 | }, 55 | ], 56 | }) 57 | @IsArray() 58 | @Type(() => VariableDto) 59 | @ValidateNested({ each: true }) 60 | @IsOptional() 61 | variable?: VariableDto[]; 62 | } 63 | -------------------------------------------------------------------------------- /src/modules/workspace/payloads/feature.payload.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty, IsBoolean } from "class-validator"; 2 | import { ApiProperty } from "@nestjs/swagger"; 3 | 4 | /** 5 | * Data Transfer Object for adding a new feature. 6 | * 7 | * This class defines the structure and validation rules for the data 8 | * required to add a new feature. 9 | */ 10 | export class AddFeatureDto { 11 | /** 12 | * The name of the feature. 13 | * 14 | * @example "Feature name" 15 | */ 16 | @IsString() 17 | @ApiProperty({ required: true, example: "Feature name" }) 18 | @IsNotEmpty() 19 | name: string; 20 | 21 | /** 22 | * Indicates if the feature is enabled. 23 | * 24 | * @example true 25 | */ 26 | @IsBoolean() 27 | @ApiProperty({ required: true, example: true }) 28 | @IsNotEmpty() 29 | isEnabled: boolean; 30 | } 31 | 32 | /** 33 | * Data Transfer Object for updating an existing feature. 34 | * 35 | * This class defines the structure and validation rules for the data 36 | * required to update an existing feature. 37 | */ 38 | export class UpdateFeatureDto { 39 | /** 40 | * Indicates if the feature is enabled. 41 | * 42 | * @example true 43 | */ 44 | @IsBoolean() 45 | @ApiProperty({ required: true, example: true }) 46 | @IsNotEmpty() 47 | isEnabled: boolean; 48 | } 49 | -------------------------------------------------------------------------------- /src/modules/workspace/payloads/feedback.payload.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum, IsNotEmpty, IsOptional, IsString } from "class-validator"; 2 | import { ApiProperty } from "@nestjs/swagger"; 3 | 4 | // ---- Enum 5 | import { 6 | FeebackSubCategory, 7 | FeedbackType, 8 | } from "@src/modules/common/enum/feedback.enum"; 9 | 10 | /** 11 | * Data transfer object (DTO) for adding feedback. 12 | */ 13 | export class AddFeedbackDto { 14 | /** 15 | * The type of feedback (e.g., suggestion, bug report). 16 | */ 17 | @ApiProperty() 18 | @IsString() 19 | @IsNotEmpty() 20 | @IsEnum(FeedbackType) 21 | type: string; 22 | 23 | /** 24 | * The sub-category of the feedback (e.g., Performance, functionality). 25 | */ 26 | @ApiProperty() 27 | @IsString() 28 | @IsOptional() 29 | @IsEnum(FeebackSubCategory) 30 | subCategory?: string; 31 | 32 | /** 33 | * The subject of the feedback (optional). 34 | */ 35 | @ApiProperty() 36 | @IsString() 37 | @IsOptional() 38 | subject?: string; 39 | 40 | /** 41 | * The description of the feedback (optional). 42 | */ 43 | @ApiProperty() 44 | @IsString() 45 | @IsOptional() 46 | description?: string; 47 | } 48 | -------------------------------------------------------------------------------- /src/modules/workspace/payloads/mock-server.payload.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator"; 2 | 3 | export class MockRequestResponseDto { 4 | @IsNumber() 5 | @IsNotEmpty() 6 | status: number; 7 | 8 | @IsString() 9 | @IsNotEmpty() 10 | body: string; 11 | 12 | @IsString() 13 | @IsOptional() 14 | contentType?: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/workspace/payloads/updates.payload.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum, IsMongoId, IsNotEmpty, IsString } from "class-validator"; 2 | import { ApiProperty } from "@nestjs/swagger"; 3 | 4 | // ---- Enum 5 | import { UpdatesType } from "@src/modules/common/enum/updates.enum"; 6 | 7 | /** 8 | * Data transfer object (DTO) for adding updates. 9 | */ 10 | export class AddUpdateDto { 11 | /** 12 | * The type of updates (e.g., workspace, collection, role). 13 | */ 14 | @ApiProperty() 15 | @IsString() 16 | @IsNotEmpty() 17 | @IsEnum(UpdatesType) 18 | type: UpdatesType; 19 | 20 | /** 21 | * Update message. 22 | */ 23 | @ApiProperty() 24 | @IsString() 25 | @IsNotEmpty() 26 | message: string; 27 | 28 | /** 29 | * workspace id where these update belong. 30 | */ 31 | @ApiProperty() 32 | @IsMongoId() 33 | @IsNotEmpty() 34 | workspaceId: string; 35 | } 36 | 37 | export class GetUpdatesDto { 38 | /** 39 | * workspace id where these update belong. 40 | */ 41 | @ApiProperty() 42 | @IsMongoId() 43 | @IsNotEmpty() 44 | workspaceId: string; 45 | 46 | /** 47 | * Page number for pagination to get the specific data . 48 | */ 49 | @ApiProperty() 50 | @IsNotEmpty() 51 | @IsString() 52 | pageNumber: string; 53 | } 54 | -------------------------------------------------------------------------------- /src/modules/workspace/payloads/workspace.payload.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { 3 | AdminDto, 4 | WorkspaceType, 5 | } from "@src/modules/common/models/workspace.model"; 6 | import { 7 | IsArray, 8 | IsBoolean, 9 | IsDateString, 10 | IsEmail, 11 | IsEnum, 12 | IsMongoId, 13 | IsNotEmpty, 14 | IsOptional, 15 | IsString, 16 | ValidateNested, 17 | } from "class-validator"; 18 | import { Type } from "class-transformer"; 19 | 20 | export class CreateWorkspaceDto { 21 | @ApiProperty({ example: "64f878a0293b1e4415866493" }) 22 | @IsMongoId() 23 | @IsNotEmpty() 24 | id: string; 25 | 26 | @ApiProperty({ 27 | example: "workspace 1", 28 | }) 29 | @IsString() 30 | @IsNotEmpty() 31 | name: string; 32 | 33 | @IsOptional() 34 | @IsArray() 35 | users?: string[]; 36 | 37 | @IsDateString() 38 | @IsOptional() 39 | createdAt?: Date; 40 | 41 | @IsMongoId() 42 | @IsOptional() 43 | createdBy?: string; 44 | 45 | @IsBoolean() 46 | @IsOptional() 47 | firstWorkspace?: boolean; 48 | 49 | @ApiProperty({ 50 | example: "This is the default workspace for the team.", 51 | required: false, 52 | }) 53 | @IsOptional() 54 | @IsString() 55 | description?: string; 56 | } 57 | 58 | export class UpdateWorkspaceDto { 59 | @ApiProperty({ 60 | example: "workspace 1", 61 | }) 62 | @IsString() 63 | @IsOptional() 64 | name?: string; 65 | 66 | @ApiProperty({ 67 | example: "Description of Workspace", 68 | }) 69 | @IsString() 70 | @IsOptional() 71 | description?: string; 72 | 73 | @IsOptional() 74 | @IsArray() 75 | users?: string[]; 76 | 77 | @IsDateString() 78 | @IsOptional() 79 | createdAt?: Date; 80 | 81 | @IsMongoId() 82 | @IsOptional() 83 | createdBy?: string; 84 | } 85 | 86 | export class UserDto { 87 | @IsMongoId() 88 | @IsNotEmpty() 89 | id: string; 90 | 91 | @IsNotEmpty() 92 | @IsString() 93 | role: string; 94 | 95 | @IsNotEmpty() 96 | @IsString() 97 | name: string; 98 | 99 | @IsEmail() 100 | @IsNotEmpty() 101 | email: string; 102 | } 103 | 104 | export class WorkspaceDtoForIdDocument { 105 | @IsMongoId() 106 | @IsOptional() 107 | id?: string; 108 | 109 | @IsString() 110 | @IsNotEmpty() 111 | name?: string; 112 | 113 | @IsArray() 114 | @Type(() => AdminDto) 115 | @ValidateNested({ each: true }) 116 | @IsOptional() 117 | admins?: AdminDto[]; 118 | 119 | @IsArray() 120 | @Type(() => UserDto) 121 | @ValidateNested({ each: true }) 122 | @IsOptional() 123 | users?: UserDto[]; 124 | 125 | @IsDateString() 126 | @IsOptional() 127 | createdAt?: Date; 128 | 129 | @IsMongoId() 130 | @IsOptional() 131 | createdBy?: string; 132 | } 133 | 134 | export class workspaceUsersResponseDto { 135 | @IsMongoId() 136 | @IsNotEmpty() 137 | id: string; 138 | 139 | @IsString() 140 | @IsNotEmpty() 141 | name: string; 142 | 143 | @IsEmail() 144 | @IsNotEmpty() 145 | email: string; 146 | 147 | @IsString() 148 | @IsNotEmpty() 149 | role: string; 150 | 151 | @IsMongoId() 152 | @IsNotEmpty() 153 | workspaceId: string; 154 | } 155 | 156 | export class UpdateWorkspaceTypeDto { 157 | @ApiProperty({ enum: WorkspaceType }) 158 | @IsString() 159 | @IsNotEmpty() 160 | @IsEnum(WorkspaceType) 161 | workspaceType: WorkspaceType; 162 | } 163 | -------------------------------------------------------------------------------- /src/modules/workspace/repositories/ai-assistant.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | @Injectable() 3 | export class AiAssistantRepository { 4 | constructor() {} 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/workspace/repositories/ai-log.repository.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "@nestjs/common"; 2 | import { Db, InsertOneResult } from "mongodb"; 3 | 4 | // ---- Enum 5 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 6 | 7 | // ---- Model 8 | import { AiLogs } from "@src/modules/common/models/ai-log.model"; 9 | 10 | // ---- Services 11 | import { ContextService } from "@src/modules/common/services/context.service"; 12 | 13 | /** 14 | * ChatbotStatsRepository 15 | * 16 | * Repository class for handling chatbot statistics related operations with MongoDB. 17 | */ 18 | @Injectable() 19 | export class AiLogRepository { 20 | /** 21 | * Constructor for ChatbotStatsRepository. 22 | * @param db The MongoDB database connection injected by the NestJS dependency injection system. 23 | * @param contextService The service for accessing context-related information like the current user. 24 | */ 25 | constructor( 26 | @Inject("DATABASE_CONNECTION") private db: Db, 27 | private readonly contextService: ContextService, 28 | ) {} 29 | 30 | async addLogs( 31 | payload: AiLogs, 32 | userId: string = null, 33 | ): Promise> { 34 | const defaultParams = { 35 | createdAt: new Date(), 36 | createdBy: this.contextService.get("user")?._id ?? userId, 37 | }; 38 | 39 | const data = await this.db 40 | .collection(Collections.AILOGS) 41 | .insertOne({ ...payload, ...defaultParams }); 42 | 43 | return data; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/modules/workspace/repositories/chatbot-stats.repositoy.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "@nestjs/common"; 2 | import { Db, InsertOneResult, ObjectId, UpdateResult } from "mongodb"; 3 | 4 | // ---- Enum 5 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 6 | 7 | // ---- Model 8 | import { ChatBotStats } from "@src/modules/common/models/chatbot-stats.model"; 9 | 10 | // ---- Payload 11 | import { UpdateChatbotDto } from "../payloads/chatbot-stats.payload"; 12 | 13 | // ---- Services 14 | import { ContextService } from "@src/modules/common/services/context.service"; 15 | 16 | /** 17 | * ChatbotStatsRepository 18 | * 19 | * Repository class for handling chatbot statistics related operations with MongoDB. 20 | */ 21 | @Injectable() 22 | export class ChatbotStatsRepository { 23 | /** 24 | * Constructor for ChatbotStatsRepository. 25 | * @param db The MongoDB database connection injected by the NestJS dependency injection system. 26 | * @param contextService The service for accessing context-related information like the current user. 27 | */ 28 | constructor( 29 | @Inject("DATABASE_CONNECTION") private db: Db, 30 | private readonly contextService: ContextService, 31 | ) {} 32 | 33 | /** 34 | * Add a new statistics entry in the chatbot stats collection. 35 | * @param chatbotstats The chatbotstats document to be inserted. 36 | * @returns The result of the insertion, including the ID of the inserted document. 37 | */ 38 | async addStats( 39 | chatbotstats: ChatBotStats, 40 | ): Promise> { 41 | const response = await this.db 42 | .collection(Collections.CHATBOTSTATS) 43 | .insertOne(chatbotstats); 44 | return response; 45 | } 46 | 47 | /** 48 | * Retrieve statistics by user ID. 49 | * @param userId The ID of the user whose statistics are to be retrieved. 50 | * @returns The statistics document associated with the given user ID. 51 | */ 52 | async getStatsByUserID(userId: string) { 53 | const data = await this.db 54 | .collection(Collections.CHATBOTSTATS) 55 | .findOne({ userId }); 56 | return data; 57 | } 58 | 59 | /** 60 | * Update statistics for a given document ID. 61 | * @param id The ObjectId of the document to be updated. 62 | * @param payload The stats data to update the document with. 63 | * @returns The result of the update operation. 64 | */ 65 | async updateStats( 66 | id: ObjectId, 67 | payload: UpdateChatbotDto, 68 | userId: string = null, 69 | ): Promise { 70 | const defaultParams = { 71 | updatedAt: new Date(), 72 | updatedBy: this.contextService.get("user")?._id ?? userId, 73 | }; 74 | const data = await this.db 75 | .collection(Collections.CHATBOTSTATS) 76 | .updateOne({ _id: id }, { $set: { ...payload, ...defaultParams } }); 77 | return data; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/modules/workspace/repositories/environment.repository.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Inject, Injectable } from "@nestjs/common"; 2 | 3 | import { 4 | Db, 5 | DeleteResult, 6 | InsertOneResult, 7 | ObjectId, 8 | UpdateResult, 9 | WithId, 10 | } from "mongodb"; 11 | 12 | import { Environment } from "@src/modules/common/models/environment.model"; 13 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 14 | import { UpdateEnvironmentDto } from "../payloads/environment.payload"; 15 | import { ContextService } from "@src/modules/common/services/context.service"; 16 | 17 | @Injectable() 18 | export class EnvironmentRepository { 19 | constructor( 20 | @Inject("DATABASE_CONNECTION") private db: Db, 21 | private readonly contextService: ContextService, 22 | ) {} 23 | 24 | async addEnvironment(environment: Environment): Promise { 25 | const response = await this.db 26 | .collection(Collections.ENVIRONMENT) 27 | .insertOne(environment); 28 | return response; 29 | } 30 | 31 | async get(id: string): Promise> { 32 | const _id = new ObjectId(id); 33 | const data = await this.db 34 | .collection(Collections.ENVIRONMENT) 35 | .findOne({ _id }); 36 | if (!data) { 37 | throw new BadRequestException("Environment Not Found"); 38 | } 39 | return data; 40 | } 41 | 42 | async delete(id: string): Promise { 43 | const _id = new ObjectId(id); 44 | const data = await this.db 45 | .collection(Collections.ENVIRONMENT) 46 | .deleteOne({ _id }); 47 | return data; 48 | } 49 | 50 | async update( 51 | id: string, 52 | updateEnvironmentDto: Partial, 53 | ): Promise { 54 | const environmentId = new ObjectId(id); 55 | const defaultParams = { 56 | updatedAt: new Date(), 57 | updatedBy: this.contextService.get("user").name, 58 | }; 59 | const data = await this.db 60 | .collection(Collections.ENVIRONMENT) 61 | .updateOne( 62 | { _id: environmentId }, 63 | { $set: { ...updateEnvironmentDto, ...defaultParams } }, 64 | ); 65 | return data; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/modules/workspace/repositories/feature.repository.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "@nestjs/common"; 2 | 3 | import { 4 | Db, 5 | DeleteResult, 6 | InsertOneResult, 7 | UpdateResult, 8 | WithId, 9 | } from "mongodb"; 10 | // ---- Enum 11 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 12 | 13 | // ---- Service 14 | import { ContextService } from "@src/modules/common/services/context.service"; 15 | 16 | // ---- Model and Payload 17 | import { Feature } from "@src/modules/common/models/feature.model"; 18 | import { UpdateFeatureDto } from "../payloads/feature.payload"; 19 | 20 | /** 21 | * Repository class for Feature operations. 22 | * 23 | * This class handles database interactions for features, 24 | * including adding, retrieving, updating, and deleting features. 25 | */ 26 | @Injectable() 27 | export class FeatureRepository { 28 | constructor( 29 | @Inject("DATABASE_CONNECTION") private db: Db, 30 | private readonly contextService: ContextService, 31 | ) {} 32 | 33 | /** 34 | * Adds a new feature to the database. 35 | * 36 | * @param feature - Feature to be added. 37 | * @returns Result of the insert operation. 38 | */ 39 | async addFeature(feature: Feature): Promise> { 40 | const response = await this.db 41 | .collection(Collections.FEATURES) 42 | .insertOne(feature); 43 | return response; 44 | } 45 | 46 | /** 47 | * Retrieves a feature by its name. 48 | * 49 | * @param name - Name of the feature. 50 | * @returns The feature with the specified name. 51 | */ 52 | async getFeatureByName(name: string): Promise> { 53 | const data = await this.db 54 | .collection(Collections.FEATURES) 55 | .findOne({ name }); 56 | return data; 57 | } 58 | 59 | /** 60 | * Deletes a feature by its name. 61 | * 62 | * @param name - Name of the feature to be deleted. 63 | * @returns Result of the delete operation. 64 | */ 65 | async deleteFeature(name: string): Promise { 66 | const data = await this.db 67 | .collection(Collections.FEATURES) 68 | .deleteOne({ name }); 69 | return data; 70 | } 71 | 72 | /** 73 | * Updates a feature by its name. 74 | * 75 | * @param name - Name of the feature to be updated. 76 | * @param updateFeatureDto - Data transfer object containing the updated feature details. 77 | * @returns Result of the update operation. 78 | */ 79 | async updateFeature( 80 | name: string, 81 | updateFeatureDto: UpdateFeatureDto, 82 | ): Promise { 83 | const defaultParams = { 84 | updatedAt: new Date(), 85 | updatedBy: this.contextService.get("user")._id, 86 | }; 87 | const data = await this.db 88 | .collection(Collections.FEATURES) 89 | .updateOne( 90 | { name: name }, 91 | { $set: { ...updateFeatureDto, ...defaultParams } }, 92 | ); 93 | return data; 94 | } 95 | 96 | /** 97 | * Retrieves all features from the database. 98 | * 99 | * @returns An array of all features. 100 | */ 101 | async getAllFeatures(): Promise[]> { 102 | const data = await this.db 103 | .collection(Collections.FEATURES) 104 | .find() 105 | .toArray(); 106 | return data; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/modules/workspace/repositories/feedback.repository.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "@nestjs/common"; 2 | import { Db, InsertOneResult } from "mongodb"; 3 | 4 | // ---- Enum 5 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 6 | 7 | // ---- Model 8 | import { Feedback } from "@src/modules/common/models/feedback.model"; 9 | 10 | /** 11 | * Feedback Repository 12 | */ 13 | @Injectable() 14 | export class FeedbackRepository { 15 | /** 16 | * Constructor for Feedback Repository. 17 | * @param db The MongoDB database connection injected by the NestJS dependency injection system. 18 | */ 19 | constructor(@Inject("DATABASE_CONNECTION") private db: Db) {} 20 | 21 | /** 22 | * Add feedback in the Feedback collection. 23 | * @param feedback The feedback document to be inserted. 24 | * @returns Inserted document ID. 25 | */ 26 | async addFeedback(feedback: Feedback): Promise> { 27 | const response = await this.db 28 | .collection(Collections.FEEDBACK) 29 | .insertOne(feedback); 30 | return response; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/workspace/repositories/updates.repository.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "@nestjs/common"; 2 | import { Db, InsertOneResult, WithId } from "mongodb"; 3 | 4 | // ---- Enum 5 | import { Collections } from "@src/modules/common/enum/database.collection.enum"; 6 | 7 | // ---- Model 8 | import { Updates } from "@src/modules/common/models/updates.model"; 9 | 10 | /** 11 | * Updates Repository 12 | */ 13 | @Injectable() 14 | export class UpdatesRepository { 15 | /** 16 | * Constructor for Updates Repository. 17 | * @param db The MongoDB database connection injected by the NestJS dependency injection system. 18 | */ 19 | constructor(@Inject("DATABASE_CONNECTION") private db: Db) {} 20 | 21 | /** 22 | * Add update in the Updates collection. 23 | * @param update The update document to be inserted. 24 | * @returns Inserted document with ID. 25 | */ 26 | async addUpdate(update: Updates): Promise> { 27 | const response = await this.db 28 | .collection(Collections.UPDATES) 29 | .insertOne(update); 30 | return response; 31 | } 32 | 33 | /** 34 | * Get paginated updates based on workspace ID. 35 | * @param workspaceId The workspace ID to filter updates. 36 | * @param skip Number of documents to skip. 37 | * @param limit Number of documents to fetch. 38 | * @returns Array of updates. 39 | */ 40 | async getPaginatedUpdates( 41 | workspaceId: string, 42 | skip: number, 43 | limit: number, 44 | ): Promise[]> { 45 | const query = { workspaceId }; 46 | const resposne = this.db 47 | .collection(Collections.UPDATES) 48 | .find(query) 49 | .sort({ createdAt: -1 }) 50 | .skip(skip) 51 | .limit(limit) 52 | .toArray(); 53 | return resposne; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/modules/workspace/services/ai-log.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | 3 | // ---- Models and Payloads 4 | import { 5 | LogDTO, 6 | } from "../payloads/ai-log.payload"; 7 | 8 | // ---- Repository 9 | import { AiLogRepository } from "../repositories/ai-log.repository"; 10 | 11 | 12 | @Injectable() 13 | export class AiLogService { 14 | 15 | constructor( 16 | private readonly ailogrepository: AiLogRepository 17 | ) {} 18 | 19 | async addLog(payload: LogDTO): Promise { 20 | 21 | await this.ailogrepository.addLogs( 22 | { 23 | userId: payload.userId, 24 | activity: payload.activity, 25 | model: payload.model, 26 | tokenConsumed: payload.tokenConsumed, 27 | thread_id: payload.thread_id 28 | }, 29 | payload.userId, 30 | ); 31 | } 32 | } -------------------------------------------------------------------------------- /src/modules/workspace/services/updates.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { InsertOneResult, WithId } from "mongodb"; 3 | // ---- Service 4 | import { ContextService } from "@src/modules/common/services/context.service"; 5 | 6 | // ---- Payload & Models 7 | import { AddUpdateDto } from "../payloads/updates.payload"; 8 | import { Updates } from "@src/modules/common/models/updates.model"; 9 | 10 | // ---- Repository 11 | import { UpdatesRepository } from "../repositories/updates.repository"; 12 | 13 | /** 14 | * Updates Service - Service responsible for handling operations related to updates. 15 | */ 16 | @Injectable() 17 | export class UpdatesService { 18 | /** 19 | * Constructor to initialize UpdatesService with required dependencies. 20 | * @param contextService - Injected ContextService for accessing user context. 21 | * @param updatesRepository - Injected UpdatesRepository for database operations. 22 | */ 23 | constructor( 24 | private readonly contextService: ContextService, 25 | private readonly updatesRepository: UpdatesRepository, 26 | ) {} 27 | 28 | /** 29 | * Adds a new update to the database. 30 | * @param update - The update object to be added. 31 | * @returns A promise resolving to the result of the database insertion. 32 | */ 33 | async addUpdate(update: AddUpdateDto): Promise> { 34 | const modifiedUpdate = { 35 | ...update, 36 | createdAt: new Date(), 37 | createdBy: this.contextService.get("user")._id, 38 | detailsUpdatedBy: this.contextService.get("user").name, 39 | }; 40 | const response = await this.updatesRepository.addUpdate(modifiedUpdate); 41 | return response; 42 | } 43 | 44 | /** 45 | * Retrieves updates for a specific workspace in a paginated manner. 46 | * @param workspaceId - The ID of the workspace to fetch updates for. 47 | * @param page - The page number of updates to fetch. 48 | * @returns A promise resolving to an object containing the page number and array of updates. 49 | */ 50 | async findUpdatesByWorkspace( 51 | workspaceId: string, 52 | page: string, 53 | ): Promise<{ pageNumber: number; updates: WithId[] }> { 54 | const pageNumber = parseInt(page, 10) || 1; 55 | const limit = 20; 56 | const skip = (pageNumber - 1) * limit; 57 | const updates = await this.updatesRepository.getPaginatedUpdates( 58 | workspaceId, 59 | skip, 60 | limit, 61 | ); 62 | return { 63 | pageNumber, 64 | updates, 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "nodenext", 4 | "declaration": true, 5 | "removeComments": true, 6 | "noLib": false, 7 | "allowSyntheticDefaultImports": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "ESNext", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "incremental": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": true, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false, 21 | "esModuleInterop": true, 22 | "paths": { 23 | "@src/*": ["src/*"], 24 | "@app/*": ["src/modules/app/*"], 25 | "@auth/*": ["src/modules/auth/*"], 26 | "@common/*": ["src/modules/common/*"], 27 | "@user/*": ["src/modules/user/*"], 28 | "@winston/*": ["src/modules/winston/*"], 29 | "@proxy/*": ["src/modules/proxy/*"] 30 | }, 31 | "resolveJsonModule": true 32 | }, 33 | "exclude": ["node_modules", "./dist/**/*"] 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | --------------------------------------------------------------------------------