From 0c5ab3484cb3c2f61710c5d84efb43e6ec7bf89d Mon Sep 17 00:00:00 2001 From: Midhun G S Date: Wed, 9 Jul 2025 22:36:41 +0530 Subject: [PATCH] Platform LTS Final fixes (#13221) * Cloud Blocker bugfixes (#13160) * fix * minor email fixes * settings menu fix * fixes * Bugfixes/whitelabelling apis (#13180) * white-labelling apis * removed consoles logs * reverts * fixes for white-labelling * fixes * reverted breadcrumb changes (#13194) * fixes for getting public sso configurations * fix for enable signup on cloud * Cloud Trial and Banners (#13182) * Cloud Blocker bugfixes (#13160) * fix * minor email fixes * settings menu fix * fixes * Cloud Trial and Banners * revert * initial commit * Added website onboarding APIs * moved ai onboarding controller to auth module * ee banners * fix --------- Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com> Co-authored-by: gsmithun4 * Bugfixes/minor UI fixes-CLoud (#13203) * Bugfixes/UI bugs platform 1 (#13205) * cleanup * Audit logs fix * gitignore changes * postgrest configs removed * removed unused import * improvements * fix * improved startup logs * Platform cypress fix (#13192) * Cloud Blocker bugfixes (#13160) * fix * minor email fixes * settings menu fix * fixes * Bugfixes/whitelabelling apis (#13180) * white-labelling apis * removed consoles logs * reverts * fixes for white-labelling * fixes * Cypress fix * reverted breadcrumb changes (#13194) * cypress fix * title fix * fixes for getting public sso configurations --------- Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com> Co-authored-by: gsmithun4 * deployment fix * added interfaces and permissions * Bugfixes/lts 3.6 branch 1 platform (#13238) * fix * Licensing Banners Fixes Cloud and EE (#13241) * design: Adds license buttons to header * Refactor header actions * Cloud Blocker bugfixes (#13160) * fix * minor email fixes * settings menu fix * fixes * subscription page * fix banners --------- Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com> * fix for public apps * fix * CE Instance Signup bug (#13254) * CE Instance Signup bug * improvement * fix * Add WEBSITE_SIGNUP_URL to deployment environment variables * Add WEBSITE_SIGNUP_URL to environment variables for deployment * Super admin banner fix (#13262) * Git Sync Fixes (#13249) * git-sync module changes * git sync fixes * added app resource guard * git-sync fixes * removed require feature * fix * review comment changes * ypress fix * App logo fix inside app builder * fix for subpath cache * fix (#13274) * platform-cypress-fix (#13271) * git sync fixes (#13277) * fix * Add data-cy for new components (#13289) --------- Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com> Co-authored-by: Rudhra Deep Biswas <98055396+rudeUltra@users.noreply.github.com> Co-authored-by: Ajith KV Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Co-authored-by: rohanlahori Co-authored-by: Adish M Co-authored-by: Rudra deep Biswas --- .github/workflows/cloud-frontend.yml | 2 + .nvmrc | 2 +- cypress-tests/cypress-platform.config.js | 6 +- cypress-tests/cypress.Dockerfile | 2 +- .../cypress/constants/selectors/common.js | 1 + .../apps/privateAndpublicApps.cy.js | 8 +- .../dataSources/dataSourcePermissions.cy.js | 4 +- .../workspace/workspaceConstants.cy.js | 28 +- .../commonTestcases/workspace/dashboard.cy.js | 329 +++++++-------- .../workspace/groups/basicPermissions.cy.js | 20 +- .../workspace/groups/permissions.cy.js | 8 +- .../cypress/support/utils/version.js | 3 +- .../AppBuilder/Header/CreateVersionModal.jsx | 12 +- .../_stores/slices/whiteLabellingSlice.js | 2 +- frontend/src/HomePage/AppMenu.jsx | 6 - frontend/src/HomePage/Folders.jsx | 33 +- frontend/src/HomePage/HomePage.jsx | 5 - .../_components/LegalReasonsErrorModal.jsx | 4 +- frontend/src/_helpers/authorizeWorkspace.js | 188 ++++----- .../_helpers/white-label/whiteLabelling.js | 15 +- .../src/_services/white-labelling.service.js | 24 +- frontend/src/_styles/license.scss | 1 + frontend/src/_styles/theme.scss | 17 +- frontend/src/_ui/Breadcrumbs/index.jsx | 128 +----- frontend/src/_ui/Header/index.jsx | 11 +- .../src/_ui/Icon/solidIcons/PremiumPlan.jsx | 14 + frontend/src/_ui/Icon/solidIcons/index.js | 3 + frontend/src/_ui/Layout/index.jsx | 3 + .../BaseManageGroupPermissions.jsx | 4 +- .../BaseSettingsMenu/BaseSettingsMenu.jsx | 6 +- .../components/UsersTable/UsersTable.jsx | 7 +- .../dataSources/components/List/index.jsx | 4 +- .../pages/SignupPage/SignupPage.jsx | 7 +- .../services/onboarding.service.ce.js | 1 - .../onboardingStore/onboarding.store.js | 1 - frontend/tailwind.config.js | 8 + server/.eslintignore | 2 - server/.gitignore | 5 +- server/.node-version | 2 +- .../1650485473528-PopulateSSOConfigs.ts | 43 +- ...4347284-AddInstanceLevelSSOInSSOConfigs.ts | 41 +- ...iryDateColumnInOrganizationLicenseTable.ts | 8 - ...tDatabaseTablesFromPublicToTenantSchema.ts | 2 +- .../1740401100000-SetDefaultWorkspace.ts | 15 +- .../1738235725332-AddAutoActivatedToUsers.ts | 19 + server/scripts/create-database.ts | 2 +- server/scripts/drop-database.ts | 2 +- server/src/entities/user.entity.ts | 5 +- server/src/helpers/bootstrap.helper.ts | 314 ++++++++++++++ server/src/helpers/import_export.helpers.ts | 6 +- server/src/helpers/utils.helper.ts | 5 +- server/src/main.ts | 384 +++++++++--------- server/src/modules/app-git/ability/guard.ts | 16 +- server/src/modules/app-git/ability/index.ts | 65 ++- .../src/modules/app-git/constants/feature.ts | 18 +- server/src/modules/app-git/controller.ts | 11 - .../app-git/guards/app-resource.guard.ts | 38 ++ server/src/modules/app-git/module.ts | 4 +- .../src/modules/app/constants/module-info.ts | 2 + server/src/modules/app/constants/modules.ts | 4 +- .../modules/app/guards/cloud-feature.guard.ts | 10 + .../app/validators/feature-guard.validator.ts | 13 +- server/src/modules/apps/service.ts | 16 +- server/src/modules/auth/constants/feature.ts | 12 + server/src/modules/auth/constants/index.ts | 6 + .../auth/decorators/ai-cookie.decorator.ts | 13 + server/src/modules/auth/dto/index.ts | 19 +- .../modules/auth/interfaces/IController.ts | 39 +- .../src/modules/auth/interfaces/IService.ts | 39 +- server/src/modules/auth/module.ts | 12 +- server/src/modules/auth/oauth/service.ts | 13 +- server/src/modules/auth/types/index.ts | 4 + server/src/modules/auth/website/controller.ts | 55 +++ server/src/modules/auth/website/service.ts | 28 ++ server/src/modules/data-sources/module.ts | 3 - server/src/modules/data-sources/service.ts | 7 +- server/src/modules/email/service.ts | 2 +- server/src/modules/git-sync/ability/guard.ts | 23 ++ server/src/modules/git-sync/ability/index.ts | 38 ++ .../src/modules/git-sync/constants/feature.ts | 17 + .../src/modules/git-sync/constants/index.ts | 10 + server/src/modules/git-sync/controller.ts | 3 + .../git_default.constant.ts | 0 .../gitsync_error.constant.ts | 0 server/src/modules/git-sync/module.ts | 10 +- server/src/modules/git-sync/types/index.ts | 18 + .../comment_users/comment_users.module.ts | 15 - .../ignored/comments/comment.module.ts | 19 - .../import_export_resources.module.ts | 58 --- .../instance_login_configs.module.ts | 15 - .../ignored/library_app/library_app.module.ts | 70 ---- .../org_environment_variables.module.ts | 74 ---- .../repositories/comment.repository.ts | 38 -- .../group_permission.repository.ts | 24 -- .../repositories/ssoConfigs.repository.ts | 45 -- .../ignored/repositories/thread.repository.ts | 41 -- .../src/modules/ignored/seeds/seeds.module.ts | 11 - .../modules/ignored/thread/thread.module.ts | 15 - .../ignored/tooljet_db/tooljet-db.types.ts | 251 ------------ .../ignored/tooljet_db/tooljet_db.module.ts | 52 --- .../ignored/tooljet_db/utils/helper.ts | 29 -- .../ignored/webhooks/webhooks.module.ts | 91 ----- server/src/modules/ignored/worker.module.ts | 10 - server/src/modules/licensing/ability/index.ts | 1 + .../modules/licensing/configs/LicenseBase.ts | 8 +- .../modules/licensing/constants/PlanTerms.ts | 3 + .../modules/licensing/constants/features.ts | 4 + .../src/modules/licensing/constants/index.ts | 2 + .../licensing/guards/auditLog.guard.ts | 2 +- .../licensing/guards/editorUser.guard.ts | 2 +- .../modules/licensing/guards/user.guard.ts | 2 +- server/src/modules/licensing/service.ts | 2 +- .../licensing/services/terms.service.ts | 5 +- server/src/modules/licensing/types/index.ts | 2 + server/src/modules/onboarding/service.ts | 3 +- server/src/modules/onboarding/util.service.ts | 64 +-- .../organization-payments/ability/index.ts | 13 +- .../constants/feature.ts | 6 +- .../organization-payments/constants/index.ts | 2 +- .../organization-payments/controller.ts | 45 +- .../interfaces/IController.ts | 27 ++ .../interfaces/IService.ts | 47 +++ .../modules/organization-payments/module.ts | 3 +- .../modules/organization-payments/service.ts | 66 ++- .../organization-payments/types/index.ts | 2 +- .../src/modules/organizations/controller.ts | 46 +-- .../src/modules/organizations/repository.ts | 6 +- .../session/guards/organization-auth.guard.ts | 2 +- .../types/organization-inputs.ts | 1 + ...{postgrest.conf => postgrest.conf.example} | 0 .../tooljet-db-table-operations.service.ts | 27 +- .../modules/users/repositories/repository.ts | 2 +- .../white-labelling/Interfaces/IController.ts | 10 +- .../modules/white-labelling/ability/index.ts | 4 +- .../white-labelling/constant/feature.ts | 6 +- .../modules/white-labelling/constant/index.ts | 5 +- .../src/modules/white-labelling/controller.ts | 24 +- .../guards/verifyOrgId.guard.ts | 2 +- .../modules/white-labelling/types/index.ts | 4 +- server/tsconfig.json | 34 +- 140 files changed, 1883 insertions(+), 1799 deletions(-) create mode 100644 frontend/src/_ui/Icon/solidIcons/PremiumPlan.jsx delete mode 100644 server/.eslintignore create mode 100644 server/migrations/1738235725332-AddAutoActivatedToUsers.ts create mode 100644 server/src/helpers/bootstrap.helper.ts create mode 100644 server/src/modules/app-git/guards/app-resource.guard.ts create mode 100644 server/src/modules/app/guards/cloud-feature.guard.ts create mode 100644 server/src/modules/auth/decorators/ai-cookie.decorator.ts create mode 100644 server/src/modules/auth/website/controller.ts create mode 100644 server/src/modules/auth/website/service.ts create mode 100644 server/src/modules/git-sync/ability/guard.ts create mode 100644 server/src/modules/git-sync/ability/index.ts create mode 100644 server/src/modules/git-sync/constants/feature.ts create mode 100644 server/src/modules/git-sync/constants/index.ts rename server/src/modules/git-sync/{constants => error-constants}/git_default.constant.ts (100%) rename server/src/modules/git-sync/{constants => error-constants}/gitsync_error.constant.ts (100%) create mode 100644 server/src/modules/git-sync/types/index.ts delete mode 100644 server/src/modules/ignored/comment_users/comment_users.module.ts delete mode 100644 server/src/modules/ignored/comments/comment.module.ts delete mode 100644 server/src/modules/ignored/import_export_resources/import_export_resources.module.ts delete mode 100644 server/src/modules/ignored/instance_login_configs/instance_login_configs.module.ts delete mode 100644 server/src/modules/ignored/library_app/library_app.module.ts delete mode 100644 server/src/modules/ignored/org_environment_variables/org_environment_variables.module.ts delete mode 100644 server/src/modules/ignored/repositories/comment.repository.ts delete mode 100644 server/src/modules/ignored/repositories/group_permission.repository.ts delete mode 100644 server/src/modules/ignored/repositories/ssoConfigs.repository.ts delete mode 100644 server/src/modules/ignored/repositories/thread.repository.ts delete mode 100644 server/src/modules/ignored/seeds/seeds.module.ts delete mode 100644 server/src/modules/ignored/thread/thread.module.ts delete mode 100644 server/src/modules/ignored/tooljet_db/tooljet-db.types.ts delete mode 100644 server/src/modules/ignored/tooljet_db/tooljet_db.module.ts delete mode 100644 server/src/modules/ignored/tooljet_db/utils/helper.ts delete mode 100644 server/src/modules/ignored/webhooks/webhooks.module.ts delete mode 100644 server/src/modules/ignored/worker.module.ts create mode 100644 server/src/modules/organization-payments/interfaces/IController.ts create mode 100644 server/src/modules/organization-payments/interfaces/IService.ts rename server/src/modules/tooljet-db/external-modules/postgrest/{postgrest.conf => postgrest.conf.example} (100%) diff --git a/.github/workflows/cloud-frontend.yml b/.github/workflows/cloud-frontend.yml index 9d0fda1e30..71e100c52d 100644 --- a/.github/workflows/cloud-frontend.yml +++ b/.github/workflows/cloud-frontend.yml @@ -110,6 +110,7 @@ jobs: TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }} TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }} TOOLJET_EDITION: cloud + WEBSITE_SIGNUP_URL: https://website-stage.tooljet.ai/ai-create-account - name: 🚀 Deploy to Netlify run: | @@ -128,4 +129,5 @@ jobs: SERVER_IP: ${{ secrets.CLOUD_SERVER_IP }} TJDB_SQL_MODE_DISABLE: ${{ secrets.CLOUD_TJDB_SQL_MODE_DISABLE }} TOOLJET_SERVER_URL: ${{ secrets.CLOUD_TOOLJET_SERVER_URL }} + WEBSITE_SIGNUP_URL: https://website-stage.tooljet.ai/ai-create-account TOOLJET_EDITION: cloud diff --git a/.nvmrc b/.nvmrc index 68c98aa7a7..16a4acdae1 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.18.2 \ No newline at end of file +v22.15.1 \ No newline at end of file diff --git a/cypress-tests/cypress-platform.config.js b/cypress-tests/cypress-platform.config.js index cb3575906a..6b1954140a 100644 --- a/cypress-tests/cypress-platform.config.js +++ b/cypress-tests/cypress-platform.config.js @@ -98,12 +98,8 @@ module.exports = defineConfig({ configFile: environment.configFile, specPattern: [ "cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js", - // Exclude specific files from ceTestcases/apps and ceTestcases/workspace "cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js", - "cypress/e2e/happyPath/platform/ceTestcases/**/!(*appSlug|appImport|privateAndpublicApps|version).cy.js", - // Exclude workspaceConstants.cy.js explicitly - "cypress/e2e/happyPath/platform/ceTestcases/workspace/!(*groupDuplication|workspaceConstants).cy.js", - "!cypress/e2e/happyPath/platform/ceTestcases/workspace/workspaceConstants.cy.js", + "cypress/e2e/happyPath/platform/ceTestcases/**/!(*appSlug).cy.js", "cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js", ], numTestsKeptInMemory: 1, diff --git a/cypress-tests/cypress.Dockerfile b/cypress-tests/cypress.Dockerfile index 705ae6bea6..3537f4fa7d 100644 --- a/cypress-tests/cypress.Dockerfile +++ b/cypress-tests/cypress.Dockerfile @@ -22,7 +22,7 @@ RUN git checkout ${BRANCH_NAME} RUN git submodule update --init --recursive # Checkout the same branch in submodules if it exists, otherwise stay on default branch -RUN git submodule foreach 'git checkout main' +RUN git submodule foreach 'git checkout ${BRANCH_NAME}' # Scripts for building COPY ./package.json ./package.json diff --git a/cypress-tests/cypress/constants/selectors/common.js b/cypress-tests/cypress/constants/selectors/common.js index ca0367ba44..464d0c3cf1 100644 --- a/cypress-tests/cypress/constants/selectors/common.js +++ b/cypress-tests/cypress/constants/selectors/common.js @@ -288,6 +288,7 @@ export const commonSelectors = { labelFieldAlert: (fieldName) => { return `[data-cy="${cyParamName(fieldName)}-is-required-field-alert-text"]`; }, + pageLogo: '[data-cy="page-logo"]', }; export const commonWidgetSelector = { diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js index f763211bea..171f5fc4b0 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js @@ -100,7 +100,7 @@ describe( ); // Test public access - cy.get(commonSelectors.viewerPageLogo).click(); + // cy.get(commonSelectors.viewerPageLogo).click(); cy.openApp( "appSlug", Cypress.env("workspaceId"), @@ -152,7 +152,7 @@ describe( "be.visible" ); - cy.get(commonSelectors.viewerPageLogo).click(); + // cy.get(commonSelectors.viewerPageLogo).click(); // Test public access cy.defaultWorkspaceLogin(); @@ -258,7 +258,9 @@ describe( "be.visible" ); - cy.get('[data-cy="viewer-page-logo"]').click(); + // cy.get('[data-cy="viewer-page-logo"]').click(); + cy.visit("/my-workspace"); + cy.wait(2000); logout(); cy.wait(1000); cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should( diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/dataSources/dataSourcePermissions.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/dataSources/dataSourcePermissions.cy.js index cc0cee6f6d..7ddd4e937d 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/dataSources/dataSourcePermissions.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/dataSources/dataSourcePermissions.cy.js @@ -47,8 +47,8 @@ describe("Datasource Manager", () => { data.dsName1 = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); data.dsName2 = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", ""); - const allDataSources = host.includes("8082") ? "All data sources (43)" : "All data sources (45)"; - const allDatabase = host.includes("8082") ? "Databases (18)" : "Databases (20)"; + const allDataSources = host.includes("8082") ? "All data sources (45)" : "All data sources (45)"; + const allDatabase = host.includes("8082") ? "Databases (20)" : "Databases (20)"; cy.get(commonSelectors.globalDataSourceIcon).click(); cy.get(commonSelectors.pageSectionHeader).verifyVisibleElement( diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/workspaceConstants.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/workspaceConstants.cy.js index ddd3f3d91e..33d377459b 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/workspaceConstants.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/workspaceConstants.cy.js @@ -39,6 +39,8 @@ describe("Workspace constants", () => { beforeEach(() => { cy.defaultWorkspaceLogin(); cy.skipWalkthrough(); + cy.viewport(1800, 1800); + }); it("Verify workspace constants UI and CRUD operations", () => { @@ -66,12 +68,11 @@ describe("Workspace constants", () => { }); }); - it("Verify global and secret constants in the editor, inspector, data sources, static queries, query preview, and preview", () => { + it.only("Verify global and secret constants in the editor, inspector, data sources, static queries, query preview, and preview", () => { data.workspaceName = fake.firstName; data.workspaceSlug = fake.firstName.toLowerCase().replace(/[^A-Za-z]/g, ""); cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug); cy.visit(data.workspaceSlug); - cy.viewport(1440, 960); data.appName = `${fake.companyName}-App`; // create global constants @@ -102,8 +103,8 @@ describe("Workspace constants", () => { .eq(0) .selectFile('cypress/fixtures/templates/workspace_constants.json', { force: true }); cy.get(importSelectors.importAppButton).click(); - cy.wait(5000); - + cy.wait(6000); + cy.get(commonWidgetSelector.draggableWidget('textinput1')).should('be.visible'); //Verify global constant value is resolved in component cy.get(commonWidgetSelector.draggableWidget('textinput1')) .verifyVisibleElement("have.value", "customHeader"); @@ -115,9 +116,10 @@ describe("Workspace constants", () => { cy.get(commonWidgetSelector.alertInfoText).contains( "secrets cannot be used in apps" ); - //Verify all static and datasource queries output in components + cy.wait(8000); for (let i = 3; i <= 16; i++) { + cy.wait(1000); cy.log("Verifying textinput" + i); cy.get(commonWidgetSelector.draggableWidget(`textinput${i}`)) .verifyVisibleElement("have.value", "Production environment testing"); @@ -151,16 +153,20 @@ describe("Workspace constants", () => { //Preview app and verify components cy.openInCurrentTab(commonWidgetSelector.previewButton); - cy.wait(6000); - for (let i = 3; i <= 16; i++) { + cy.wait(8000); + cy.get(commonWidgetSelector.draggableWidget('textinput1')).should('be.visible'); + for (let i = 16; i >= 3; i--) { + cy.wait(1000); + cy.get(commonWidgetSelector.draggableWidget(`textinput${i}`)).should('be.visible'); cy.get(commonWidgetSelector.draggableWidget(`textinput${i}`)) - .verifyVisibleElement("have.value", "Production environment testing"); + .verifyVisibleElement("have.value", "Production environment testing", { timeout: 10000 }); } - //back to dashboard and open app again - cy.get(commonSelectors.viewerPageLogo).click(); - cy.wait(2000); + + cy.visit('/'); + cy.wait(4000); cy.get(commonSelectors.appEditButton).click({ force: true }); + cy.wait(4000); cy.releaseApp(); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js index fb8e932973..dc0ba39e61 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/dashboard.cy.js @@ -24,6 +24,7 @@ import { logout } from "Support/utils/common"; describe("dashboard", () => { let data = {}; + beforeEach(() => { data = { appName: `${fake.companyName}-App`, @@ -44,164 +45,6 @@ describe("dashboard", () => { cy.visit(`${data.workspaceSlug}`); }); - // it("Should verify app card elements and app card operations", () => { - // const customLayout = { - // desktop: { top: 100, left: 20 }, - // mobile: { width: 8, height: 50 }, - // }; - - // cy.apiCreateApp(data.appName); - // cy.visit(`${data.workspaceSlug}`); - - // cy.wait(2000); - // cy.get(commonSelectors.appCreationDetails).should("be.visible"); - // cy.get(commonSelectors.appCard(data.appName)).should("be.visible"); - // cy.get(commonSelectors.appTitle(data.appName)).verifyVisibleElement( - // "have.text", - // data.appName - // ); - - // viewAppCardOptions(data.appName); - // cy.get( - // commonSelectors.appCardOptions(commonText.changeIconOption) - // ).verifyVisibleElement("have.text", commonText.changeIconOption); - // cy.get( - // commonSelectors.appCardOptions(commonText.addToFolderOption) - // ).verifyVisibleElement("have.text", commonText.addToFolderOption); - // cy.get( - // commonSelectors.appCardOptions(commonText.cloneAppOption) - // ).verifyVisibleElement("have.text", commonText.cloneAppOption); - // cy.get( - // commonSelectors.appCardOptions(commonText.exportAppOption) - // ).verifyVisibleElement("have.text", commonText.exportAppOption); - // cy.get( - // commonSelectors.appCardOptions(commonText.deleteAppOption) - // ).verifyVisibleElement("have.text", commonText.deleteAppOption); - - // modifyAndVerifyAppCardIcon(data.appName); - // createFolder(data.folderName); - - // viewAppCardOptions(data.appName); - // cy.get( - // commonSelectors.appCardOptions(commonText.addToFolderOption) - // ).click(); - // verifyModal( - // dashboardText.addToFolderTitle, - // dashboardText.addToFolderButton, - // dashboardSelector.selectFolder - // ); - // cy.get(dashboardSelector.moveAppText).verifyVisibleElement( - // "have.text", - // dashboardText.moveAppText(data.appName) - // ); - - // cy.get(dashboardSelector.selectFolder).click(); - // cy.get(commonSelectors.folderList).contains(data.folderName).click(); - // cy.get(dashboardSelector.addToFolderButton).click(); - // cy.verifyToastMessage( - // commonSelectors.toastMessage, - // commonText.AddedToFolderToast, - // false - // ); - - // cy.get(dashboardSelector.folderName(data.folderName)).verifyVisibleElement( - // "have.text", - // dashboardText.folderName(`${data.folderName} (1)`) - // ); - - // cy.get(dashboardSelector.folderName(data.folderName)).click(); - // cy.get(commonSelectors.appCard(data.appName)) - // .contains(data.appName) - // .should("be.visible"); - - // viewAppCardOptions(data.appName); - - // cy.get(commonSelectors.appCardOptions(commonText.removeFromFolderOption)) - // .verifyVisibleElement("have.text", commonText.removeFromFolderOption) - // .click(); - // verifyConfirmationModal(commonText.appRemovedFromFolderMessage); - - // cancelModal(commonText.cancelButton); - - // viewAppCardOptions(data.appName); - // cy.get( - // commonSelectors.appCardOptions(commonText.removeFromFolderOption) - // ).click(); - // cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click(); - // cy.verifyToastMessage( - // commonSelectors.toastMessage, - // commonText.appRemovedFromFolderTaost, - // false - // ); - // cy.get(commonSelectors.modalComponent).should("not.exist"); - // cy.get(commonSelectors.empytyFolderImage).should("be.visible"); - // cy.get(commonSelectors.emptyFolderText).verifyVisibleElement( - // "have.text", - // commonText.emptyFolderText - // ); - // cy.get(commonSelectors.allApplicationsLink).click(); - // deleteFolder(data.folderName); - - // cy.get(commonSelectors.allApplicationsLink).click(); - - // cy.wait(1000); - // viewAppCardOptions(data.appName); - // cy.wait(2000); - // cy.get(commonSelectors.appCardOptions(commonText.exportAppOption)).click(); - // cy.get(commonSelectors.exportAllButton).click(); - - // cy.exec("ls ./cypress/downloads/").then((result) => { - // const downloadedAppExportFileName = result.stdout.split("\n")[0]; - // expect(downloadedAppExportFileName).to.contain.string("app"); - // }); - - // viewAppCardOptions(data.appName); - // cy.get(commonSelectors.appCardOptions(commonText.cloneAppOption)).click(); - // cy.get('[data-cy="clone-app"]').click(); - // cy.get(".go3958317564") - // .should("be.visible") - // .and("have.text", dashboardText.appClonedToast); - // cy.wait(3000); - - // cy.renameApp(data.cloneAppName); - // cy.apiAddComponentToApp(data.cloneAppName, "button", 25, 25); - // cy.backToApps(); - // cy.wait("@appLibrary"); - // cy.wait(1000); - - // cy.get(commonSelectors.appCard(data.cloneAppName)).should("be.visible"); - - // cy.wait(1000); - - // viewAppCardOptions(data.cloneAppName); - // cy.get(commonSelectors.deleteAppOption).click(); - // cy.get(commonSelectors.modalMessage).verifyVisibleElement( - // "have.text", - // commonText.deleteAppModalMessage(data.cloneAppName) - // ); - // cy.get( - // commonSelectors.buttonSelector(commonText.cancelButton) - // ).verifyVisibleElement("have.text", commonText.cancelButton); - // cy.get( - // commonSelectors.buttonSelector(commonText.modalYesButton) - // ).verifyVisibleElement("have.text", commonText.modalYesButton); - // cancelModal(commonText.cancelButton); - - // viewAppCardOptions(data.cloneAppName); - // cy.get(commonSelectors.deleteAppOption).click(); - // cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click(); - // cy.verifyToastMessage( - // commonSelectors.toastMessage, - // commonText.appDeletedToast, - // false - // ); - // verifyAppDelete(data.cloneAppName); - // cy.wait("@appLibrary"); - - // cy.deleteApp(data.appName); - // verifyAppDelete(data.appName); - // }); - it("should verify the elements on empty dashboard", () => { cy.intercept("GET", "/api/metadata", { body: { @@ -259,9 +102,6 @@ describe("dashboard", () => { .should("have.attr", "class") .and("contain", "theme-dark"); cy.get(dashboardSelector.modeToggle).click(); - cy.get(dashboardSelector.homePageContent) - .should("have.attr", "class") - .and("contain", "bg-light-gray"); cy.wait(500); cy.get(commonSelectors.settingsIcon).click(); @@ -329,6 +169,169 @@ describe("dashboard", () => { verifyTooltip(dashboardSelector.modeToggle, "Mode"); }); + it("Should verify app card elements and app card operations", () => { + cy.exec("mkdir -p ./cypress/downloads/"); + cy.exec("cd ./cypress/downloads/ && rm -rf *"); + + const customLayout = { + desktop: { top: 100, left: 20 }, + mobile: { width: 8, height: 50 }, + }; + + cy.apiCreateApp(data.appName); + cy.visit(`${data.workspaceSlug}`); + + cy.wait(2000); + cy.get(commonSelectors.appCreationDetails).should("be.visible"); + cy.get(commonSelectors.appCard(data.appName)).should("be.visible"); + cy.get(commonSelectors.appTitle(data.appName)).verifyVisibleElement( + "have.text", + data.appName + ); + + viewAppCardOptions(data.appName); + cy.get( + commonSelectors.appCardOptions(commonText.changeIconOption) + ).verifyVisibleElement("have.text", commonText.changeIconOption); + cy.get( + commonSelectors.appCardOptions(commonText.addToFolderOption) + ).verifyVisibleElement("have.text", commonText.addToFolderOption); + cy.get( + commonSelectors.appCardOptions(commonText.cloneAppOption) + ).verifyVisibleElement("have.text", commonText.cloneAppOption); + cy.get( + commonSelectors.appCardOptions(commonText.exportAppOption) + ).verifyVisibleElement("have.text", commonText.exportAppOption); + cy.get( + commonSelectors.appCardOptions(commonText.deleteAppOption) + ).verifyVisibleElement("have.text", commonText.deleteAppOption); + + modifyAndVerifyAppCardIcon(data.appName); + createFolder(data.folderName); + + viewAppCardOptions(data.appName); + cy.get( + commonSelectors.appCardOptions(commonText.addToFolderOption) + ).click(); + verifyModal( + dashboardText.addToFolderTitle, + dashboardText.addToFolderButton, + dashboardSelector.selectFolder + ); + cy.get(dashboardSelector.moveAppText).verifyVisibleElement( + "have.text", + dashboardText.moveAppText(data.appName) + ); + + cy.get(dashboardSelector.selectFolder).click(); + cy.get(commonSelectors.folderList).contains(data.folderName).click(); + cy.get(dashboardSelector.addToFolderButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + commonText.AddedToFolderToast, + false + ); + + cy.get(dashboardSelector.folderName(data.folderName)).verifyVisibleElement( + "have.text", + dashboardText.folderName(`${data.folderName} (1)`) + ); + + cy.get(dashboardSelector.folderName(data.folderName)).click(); + cy.get(commonSelectors.appCard(data.appName)) + .contains(data.appName) + .should("be.visible"); + + viewAppCardOptions(data.appName); + + cy.get(commonSelectors.appCardOptions(commonText.removeFromFolderOption)) + .verifyVisibleElement("have.text", commonText.removeFromFolderOption) + .click(); + verifyConfirmationModal(commonText.appRemovedFromFolderMessage); + + cancelModal(commonText.cancelButton); + + viewAppCardOptions(data.appName); + cy.get( + commonSelectors.appCardOptions(commonText.removeFromFolderOption) + ).click(); + cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + commonText.appRemovedFromFolderTaost, + false + ); + cy.get(commonSelectors.modalComponent).should("not.exist"); + cy.get(commonSelectors.empytyFolderImage).should("be.visible"); + cy.get(commonSelectors.emptyFolderText).verifyVisibleElement( + "have.text", + commonText.emptyFolderText + ); + cy.get(commonSelectors.allApplicationsLink).click(); + deleteFolder(data.folderName); + + cy.get(commonSelectors.allApplicationsLink).click(); + + cy.wait(1000); + viewAppCardOptions(data.appName); + cy.wait(2000); + cy.get(commonSelectors.appCardOptions(commonText.exportAppOption)).click(); + cy.get(commonSelectors.exportAllButton).click(); + + + cy.exec("ls ./cypress/downloads/").then((result) => { + const downloadedAppExportFileName = result.stdout.split("\n")[0]; + expect(downloadedAppExportFileName).to.contain.string("app"); + }); + + viewAppCardOptions(data.appName); + cy.get(commonSelectors.appCardOptions(commonText.cloneAppOption)).click(); + cy.get('[data-cy="clone-app"]').click(); + cy.get(".go3958317564") + .should("be.visible") + .and("have.text", dashboardText.appClonedToast); + cy.wait(3000); + + cy.renameApp(data.cloneAppName); + cy.apiAddComponentToApp(data.cloneAppName, "button", 25, 25); + cy.backToApps(); + cy.wait("@appLibrary"); + cy.wait(1000); + + cy.get(commonSelectors.appCard(data.cloneAppName)).should("be.visible"); + + cy.wait(1000); + + viewAppCardOptions(data.cloneAppName); + cy.get(commonSelectors.deleteAppOption).click(); + cy.get(commonSelectors.modalMessage).verifyVisibleElement( + "have.text", + commonText.deleteAppModalMessage(data.cloneAppName) + ); + cy.get( + commonSelectors.buttonSelector(commonText.cancelButton) + ).verifyVisibleElement("have.text", commonText.cancelButton); + cy.get( + commonSelectors.buttonSelector(commonText.modalYesButton) + ).verifyVisibleElement("have.text", commonText.modalYesButton); + cancelModal(commonText.cancelButton); + + viewAppCardOptions(data.cloneAppName); + cy.get(commonSelectors.deleteAppOption).click(); + cy.get(commonSelectors.buttonSelector(commonText.modalYesButton)).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + commonText.appDeletedToast, + false + ); + + cy.get(commonSelectors.appCard(data.cloneAppName)).should("not.exist"); + cy.wait("@appLibrary"); + + cy.deleteApp(data.appName); + cy.get(commonSelectors.appCard(data.appName)).should("not.exist"); + }); + it("Should verify the app CRUD operation", () => { const customLayout = { desktop: { top: 100, left: 20 }, @@ -353,7 +356,7 @@ describe("dashboard", () => { cy.deleteApp(data.appName); - verifyAppDelete(data.appName); + cy.get(commonSelectors.appCard(data.appName)).should("not.exist"); }); it("Should verify the folder CRUD operation", () => { @@ -474,7 +477,7 @@ describe("dashboard", () => { cy.get(commonSelectors.allApplicationsLink).click(); cy.deleteApp(data.appName); - verifyAppDelete(data.appName); + cy.get(commonSelectors.appCard(data.appName)).should("not.exist"); logout(); }); }); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.cy.js index 96b87dd9b7..681a2d93e6 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/basicPermissions.cy.js @@ -76,11 +76,11 @@ describe("Manage Groups", () => { // App operations cy.createApp(data.appName); - cy.verifyToastMessage( - commonSelectors.toastMessage, - commonText.appCreatedToast, - false - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // commonText.appCreatedToast, + // false + // ); cy.backToApps(); cy.deleteApp(data.appName); @@ -178,11 +178,11 @@ describe("Manage Groups", () => { // App operations cy.createApp(data.appName); - cy.verifyToastMessage( - commonSelectors.toastMessage, - commonText.appCreatedToast, - false - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // commonText.appCreatedToast, + // false + // ); cy.backToApps(); cy.deleteApp(data.appName); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.cy.js index bfa5806939..e9a4f5c222 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/workspace/groups/permissions.cy.js @@ -196,10 +196,10 @@ describe("Manage Groups", () => { // App operations cy.createApp(data.appName); - cy.verifyToastMessage( - commonSelectors.toastMessage, - commonText.appCreatedToast - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // commonText.appCreatedToast + // ); cy.backToApps(); cy.wait(2500); diff --git a/cypress-tests/cypress/support/utils/version.js b/cypress-tests/cypress/support/utils/version.js index c6d79853fb..ffb1ad32a0 100644 --- a/cypress-tests/cypress/support/utils/version.js +++ b/cypress-tests/cypress/support/utils/version.js @@ -150,7 +150,8 @@ export const verifyVersionAfterPreview = (currentVersion) => { cy.wait(2000); cy.get('[data-cy^="draggable-widget-table"]').should("be.visible"); cy.url().should("include", `version=${currentVersion}`); - cy.get('[data-cy="viewer-page-logo"]').click(); + // cy.get('[data-cy="viewer-page-logo"]').click(); + cy.go("back"); cy.wait(8000); }; diff --git a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx index 66e62fcd8b..dd2fb8c916 100644 --- a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx +++ b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx @@ -16,11 +16,15 @@ const CreateVersionModal = ({ canCommit, orgGit, fetchingOrgGit, - handleCommitOnVersionCreation = () => { }, + handleCommitOnVersionCreation = () => {}, }) => { const { moduleId } = useModuleContext(); const [isCreatingVersion, setIsCreatingVersion] = useState(false); const [versionName, setVersionName] = useState(''); + const isGitSyncEnabled = + orgGit?.org_git?.git_ssh?.is_enabled || + orgGit?.org_git?.git_https?.is_enabled || + orgGit?.org_git?.git_lab?.is_enabled; const { createNewVersionAction, @@ -102,8 +106,8 @@ const CreateVersionModal = ({ }); }, (error) => { - if (error?.data?.code === "23505") { - toast.error("Version name already exists."); + if (error?.data?.code === '23505') { + toast.error('Version name already exists.'); } else { toast.error(error?.error); } @@ -172,7 +176,7 @@ const CreateVersionModal = ({ - {orgGit?.org_git?.is_enabled && ( + {isGitSyncEnabled && (
- {appType !== 'workflow' && ( - openAppActionModal('clone-app')} - /> - )} { setShowInput(true); }} - data-cy="create-new-folder-button" + data-cy="folder-search-icon" >
)} diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 31aa2dfee0..7e3b477798 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -1655,11 +1655,6 @@ class HomePageComponent extends React.Component {
{featuresLoaded && !isLoading ? ( <> - { const [isOpen, setShowModal] = useState(propShowModal); - const currentUser = authenticationService.currentSessionValue; const handleClose = () => { setShowModal(false); toggleModal && toggleModal(); @@ -63,7 +63,7 @@ const LegalReasonsErrorModal = ({ - {currentUser?.super_admin && ( + {actionButtonAdmin && (
)} @@ -140,20 +140,21 @@ function Header({ iconWidth="14" size="md" onClick={toggleCollapsibleSidebar} - > + />
)} -
+
+ Version {currentVersion}
diff --git a/frontend/src/_ui/Icon/solidIcons/PremiumPlan.jsx b/frontend/src/_ui/Icon/solidIcons/PremiumPlan.jsx new file mode 100644 index 0000000000..4ff4b4f8a7 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/PremiumPlan.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const PremiumPlan = ({ width = '41', height = '41' }) => ( + + + +); + +export default PremiumPlan; diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js index 6e0f40c9c9..6531303435 100644 --- a/frontend/src/_ui/Icon/solidIcons/index.js +++ b/frontend/src/_ui/Icon/solidIcons/index.js @@ -236,6 +236,7 @@ import AICrown from './AICrown.jsx'; import BookDemo from './BookDemo.jsx'; import Contactv3 from './Contactv3.jsx'; import PremiumLogo from './PremiumLogo.jsx'; +import PremiumPlan from './PremiumPlan.jsx'; import StudentIcon from './StudentIcon.jsx'; import CalendarIcon from './CalendarIcon.jsx'; import CalendarSmall from './CalendarSmall.jsx'; @@ -765,6 +766,8 @@ const Icon = (props) => { return ; case 'premium-logo': return ; + case 'premium-plan': + return ; case 'calendar-icon': return ; case 'calendar-small': diff --git a/frontend/src/_ui/Layout/index.jsx b/frontend/src/_ui/Layout/index.jsx index 1f7095dd04..5783c5510a 100644 --- a/frontend/src/_ui/Layout/index.jsx +++ b/frontend/src/_ui/Layout/index.jsx @@ -26,6 +26,7 @@ function Layout({ const [licenseValid, setLicenseValid] = useState(false); const logo = retrieveWhiteLabelLogo(); const router = useRouter(); + const [licenseStatus, setLicenseStatus] = useState(null); const { featureAccess } = useLicenseStore( (state) => ({ featureAccess: state.featureAccess, @@ -84,6 +85,7 @@ function Layout({ useEffect(() => { let licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; setLicenseValid(licenseValid); + setLicenseStatus(featureAccess?.licenseStatus); }, [featureAccess]); const currentUserValue = authenticationService.currentSessionValue; @@ -147,6 +149,7 @@ function Layout({ enableCollapsibleSidebar={enableCollapsibleSidebar} collapseSidebar={collapseSidebar} toggleCollapsibleSidebar={toggleCollapsibleSidebar} + licenseStatus={licenseStatus} />
{children}
diff --git a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx index a8f70603f7..10cb9e2583 100644 --- a/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx +++ b/frontend/src/modules/WorkspaceSettings/pages/Groups/components/BaseManageGroupPermissions/BaseManageGroupPermissions.jsx @@ -394,6 +394,8 @@ class BaseManageGroupPermissions extends React.Component { } = this.state; const { featureAccess, isFeatureEnabled, isTrial } = this.props; + const isValidLicense = featureAccess?.licenseStatus.isLicenseValid; + const planType = featureAccess?.licenseStatus?.licenseType; const grounNameErrorStyle = this.state.newGroupName?.length > 50 ? { color: '#ff0000', borderColor: '#ff0000' } : {}; @@ -797,7 +799,7 @@ class BaseManageGroupPermissions extends React.Component { )} - {!_.isEmpty(featureAccess) && !isFeatureEnabled && ( + {(!isValidLicense || planType === 'trial') && featureAccess && ( ({ @@ -77,7 +79,6 @@ function BaseSettingsMenu({ featureAccess, checkForUnsavedChanges, }); - return (
{/* Marketplace section */} @@ -101,6 +102,9 @@ function BaseSettingsMenu({ {/* Super Admin Settings */} {superAdmin && midMenuContent} + {/* Specifically for Cloud Edition */} + {edition === 'cloud' && admin && !superAdmin && midMenuContent} + {/* Admin section - Workspace settings */} {admin && ( @@ -192,7 +194,10 @@ const UsersTable = ({ > {user.status} - {user.status === 'invited' && !hideAccountSetupLink && user?.invitation_token ? ( + {user.status === 'invited' && + !hideAccountSetupLink && + user?.invitation_token && + edition != 'cloud' ? (
diff --git a/frontend/src/modules/dataSources/components/List/index.jsx b/frontend/src/modules/dataSources/components/List/index.jsx index 25f46fd226..6f44ab3cd4 100644 --- a/frontend/src/modules/dataSources/components/List/index.jsx +++ b/frontend/src/modules/dataSources/components/List/index.jsx @@ -102,7 +102,7 @@ export const List = ({ updateSelectedDatasource }) => { setFilteredData(filtered); }; - function handleClose() { + function handleClose () { setShowInput(false); setFilteredData(dataSources); } @@ -150,7 +150,7 @@ export const List = ({ updateSelectedDatasource }) => { onClick={() => { setShowInput(true); }} - data-cy="create-new-folder-button" + data-cy="added-ds-search-icon" > diff --git a/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx b/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx index 81397312df..327683f3cb 100644 --- a/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx +++ b/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx @@ -7,8 +7,11 @@ import OnboardingBackgroundWrapper from '@/modules/onboarding/components/Onboard import { onInvitedUserSignUpSuccess } from '@/_helpers/platform/utils/auth.utils'; import { SignupForm, SignupSuccessInfo } from './components'; import { GeneralFeatureImage } from '@/modules/common/components'; +import { fetchEdition } from '@/modules/common/helpers/utils'; +import * as envConfigs from 'config'; const SignupPage = ({ configs, organizationId }) => { + const edition = fetchEdition(); const { t } = useTranslation(); const location = useLocation(); const navigate = useNavigate(); @@ -18,13 +21,15 @@ const SignupPage = ({ configs, organizationId }) => { email: '', name: '', }); - const routeState = location.state; const organizationToken = routeState?.organizationToken; const inviteeEmail = routeState?.inviteeEmail; const inviteOrganizationId = organizationId; const paramInviteOrganizationSlug = params.organizationId; const redirectTo = location?.search?.split('redirectTo=')[1]; + if (!paramInviteOrganizationSlug && edition === 'cloud') { + window.location.href = envConfigs.WEBSITE_SIGNUP_URL || 'https://www.tooljet.ai/signup'; + } useEffect(() => { const errorMessage = location?.state?.errorMessage; if (errorMessage) { diff --git a/frontend/src/modules/onboarding/services/onboarding.service.ce.js b/frontend/src/modules/onboarding/services/onboarding.service.ce.js index 7e193867c3..ce100b30e5 100644 --- a/frontend/src/modules/onboarding/services/onboarding.service.ce.js +++ b/frontend/src/modules/onboarding/services/onboarding.service.ce.js @@ -17,7 +17,6 @@ function setupFirstUser({ companyName, buildPurpose, name, workspaceName, passwo password, }), }; - console.log(requestOptions.body, 'BRUH'); return fetch(`${config.apiUrl}/onboarding/setup-super-admin`, requestOptions) .then(handleResponse) diff --git a/frontend/src/modules/onboarding/stores/onboardingStore/onboarding.store.js b/frontend/src/modules/onboarding/stores/onboardingStore/onboarding.store.js index 133437ba42..62bd090e72 100644 --- a/frontend/src/modules/onboarding/stores/onboardingStore/onboarding.store.js +++ b/frontend/src/modules/onboarding/stores/onboardingStore/onboarding.store.js @@ -61,7 +61,6 @@ const useCEOnboardingStore = create( createSuperAdminAccount: async () => { if (!get().accountCreated) { const data = get().prepareSetupAdminData(); - console.log('BRUH CE', data); await setupFirstUser(data); set({ accountCreated: true }); } diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 902f7d1aa2..c12754054b 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -103,6 +103,14 @@ module.exports = { boxShadow: { 'interactive-focus-outline': ' 0px 0px 0px 2px var(--interactive-focus-outline)', 'interactive-focus-outline-inset': 'inset 0px 0px 0px 2px #fff', + 'elevation-000': '0px 1px 0px 0px rgba(0, 0, 0, 0.10)', + 'elevation-100': '0px 1px 1px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05)', + 'elevation-200': '0px 2px 4px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05)', + 'elevation-300': '0px 4px 8px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05)', + 'elevation-400': '0px 8px 16px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05)', + 'elevation-500': '0px 16px 24px 0px rgba(48, 50, 51, 0.09), 0px 0px 1px 0px rgba(48, 50, 51, 0.05)', + 'elevation-600': '0px 24px 40px 0px rgba(48, 50, 51, 0.08), 0px 0px 1px 0px rgba(48, 50, 51, 0.05)', + 'elevation-700': '0px 32px 50px 0px rgba(48, 50, 51, 0.08), 0px 0px 1px 0px rgba(48, 50, 51, 0.05)', }, fontSize: { sm: ['11px', '16px'], diff --git a/server/.eslintignore b/server/.eslintignore deleted file mode 100644 index 6782f3eb5e..0000000000 --- a/server/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -migrations \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore index c16ef026a3..0db113322a 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -31,4 +31,7 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +# Postgrest configuration +**/postgrest.conf \ No newline at end of file diff --git a/server/.node-version b/server/.node-version index c6244cda04..8320a6d299 100644 --- a/server/.node-version +++ b/server/.node-version @@ -1 +1 @@ -14.17.3 +22.15.1 diff --git a/server/data-migrations/1650485473528-PopulateSSOConfigs.ts b/server/data-migrations/1650485473528-PopulateSSOConfigs.ts index adfa7f0d1b..71929e7891 100644 --- a/server/data-migrations/1650485473528-PopulateSSOConfigs.ts +++ b/server/data-migrations/1650485473528-PopulateSSOConfigs.ts @@ -1,46 +1,59 @@ import { Organization } from '@entities/organization.entity'; import { SSOConfigs, SSOType } from '@entities/sso_config.entity'; import { MigrationInterface, QueryRunner } from 'typeorm'; -import { EncryptionService } from '@modules/encryption/service'; +import { getImportPath, TOOLJET_EDITIONS } from '@modules/app/constants'; +import { getTooljetEdition } from '@helpers/utils.helper'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from '@modules/app/module'; +import { getEnvVars } from 'scripts/database-config-utils'; export class PopulateSSOConfigs1650485473528 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const entityManager = queryRunner.manager; - const encryptionService = new EncryptionService(); - const OrganizationRepository = entityManager.getRepository(Organization); + const nestApp = await NestFactory.createApplicationContext(await AppModule.register({ IS_GET_CONTEXT: true })); - const isSingleOrganization = process.env.DISABLE_MULTI_WORKSPACE === 'true'; - const enableSignUp = process.env.SSO_DISABLE_SIGNUP !== 'true'; - const domain = process.env.SSO_RESTRICTED_DOMAIN; + const edition = getTooljetEdition() as TOOLJET_EDITIONS; + const { EncryptionService } = await import(`${await getImportPath(true, edition)}/encryption/service`); + const encryptionService = nestApp.get(EncryptionService); - const googleEnabled = !!process.env.SSO_GOOGLE_OAUTH2_CLIENT_ID; + const envVars = getEnvVars(); + + const isSingleOrganization = envVars.DISABLE_MULTI_WORKSPACE === 'true'; + const enableSignUp = envVars.SSO_DISABLE_SIGNUP !== 'true'; + const domain = envVars.SSO_RESTRICTED_DOMAIN; + + const googleEnabled = !!envVars.SSO_GOOGLE_OAUTH2_CLIENT_ID; const googleConfigs = { - clientId: process.env.SSO_GOOGLE_OAUTH2_CLIENT_ID, + clientId: envVars.SSO_GOOGLE_OAUTH2_CLIENT_ID, }; - const gitEnabled = !!process.env.SSO_GIT_OAUTH2_CLIENT_ID; + const gitEnabled = !!envVars.SSO_GIT_OAUTH2_CLIENT_ID; const gitConfigs = { - clientId: process.env.SSO_GIT_OAUTH2_CLIENT_ID, + clientId: envVars.SSO_GIT_OAUTH2_CLIENT_ID, clientSecret: - process.env.SSO_GIT_OAUTH2_CLIENT_SECRET && + envVars.SSO_GIT_OAUTH2_CLIENT_SECRET && (await encryptionService.encryptColumnValue( 'ssoConfigs', 'clientSecret', - process.env.SSO_GIT_OAUTH2_CLIENT_SECRET + envVars.SSO_GIT_OAUTH2_CLIENT_SECRET )), }; - const passwordEnabled = process.env.DISABLE_PASSWORD_LOGIN !== 'true'; + const passwordEnabled = envVars.DISABLE_PASSWORD_LOGIN !== 'true'; - const organizations: Organization[] = await OrganizationRepository.find({ + const organizations: Organization[] = await entityManager.find(Organization, { relations: ['ssoConfigs'], select: ['ssoConfigs', 'id'], }); if (organizations && organizations.length > 0) { for (const organization of organizations) { - await OrganizationRepository.update({ id: organization.id }, { enableSignUp, ...(domain ? { domain } : {}) }); + await entityManager.update( + Organization, + { id: organization.id }, + { enableSignUp, ...(domain ? { domain } : {}) } + ); // adding form configs for organizations which does not have any if ( !organization.ssoConfigs?.some((og) => { diff --git a/server/data-migrations/1706024347284-AddInstanceLevelSSOInSSOConfigs.ts b/server/data-migrations/1706024347284-AddInstanceLevelSSOInSSOConfigs.ts index 38bf6b78c6..e9abe948c5 100644 --- a/server/data-migrations/1706024347284-AddInstanceLevelSSOInSSOConfigs.ts +++ b/server/data-migrations/1706024347284-AddInstanceLevelSSOInSSOConfigs.ts @@ -1,33 +1,44 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; import { ConfigScope, SSOConfigs, SSOType } from '@entities/sso_config.entity'; -import { EncryptionService } from '@modules/encryption/service'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from '@modules/app/module'; +import { getTooljetEdition } from '@helpers/utils.helper'; +import { getImportPath, TOOLJET_EDITIONS } from '@modules/app/constants'; +import { getEnvVars } from 'scripts/database-config-utils'; export class AddInstanceLevelSSOInSSOConfigs1706024347284 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const entityManager = queryRunner.manager; - const encryptionService = new EncryptionService(); + const nestApp = await NestFactory.createApplicationContext(await AppModule.register({ IS_GET_CONTEXT: true })); + + const edition = getTooljetEdition() as TOOLJET_EDITIONS; + const { EncryptionService } = await import(`${await getImportPath(true, edition)}/encryption/service`); + const encryptionService = nestApp.get(EncryptionService); + + const envVars = getEnvVars(); + const ssoConfigs: Partial[] = [ { configScope: ConfigScope.INSTANCE, sso: SSOType.GOOGLE, - enabled: !!process.env?.SSO_GOOGLE_OAUTH2_CLIENT_ID, + enabled: !!envVars?.SSO_GOOGLE_OAUTH2_CLIENT_ID, configs: { - clientId: process.env?.SSO_GOOGLE_OAUTH2_CLIENT_ID || '', + clientId: envVars?.SSO_GOOGLE_OAUTH2_CLIENT_ID || '', }, }, { configScope: ConfigScope.INSTANCE, sso: SSOType.GIT, - enabled: !!process.env?.SSO_GIT_OAUTH2_CLIENT_ID, + enabled: !!envVars?.SSO_GIT_OAUTH2_CLIENT_ID, configs: { - clientId: process.env?.SSO_GIT_OAUTH2_CLIENT_ID || '', - hostName: process.env?.SSO_GIT_OAUTH2_HOST || '', + clientId: envVars?.SSO_GIT_OAUTH2_CLIENT_ID || '', + hostName: envVars?.SSO_GIT_OAUTH2_HOST || '', clientSecret: - (process.env?.SSO_GIT_OAUTH2_CLIENT_SECRET && + (envVars?.SSO_GIT_OAUTH2_CLIENT_SECRET && (await encryptionService.encryptColumnValue( 'ssoConfigs', 'clientSecret', - process.env.SSO_GIT_OAUTH2_CLIENT_SECRET + envVars.SSO_GIT_OAUTH2_CLIENT_SECRET ))) || '', }, @@ -35,19 +46,19 @@ export class AddInstanceLevelSSOInSSOConfigs1706024347284 implements MigrationIn { configScope: ConfigScope.INSTANCE, sso: SSOType.OPENID, - enabled: !!process.env?.SSO_OPENID_CLIENT_ID, + enabled: !!envVars?.SSO_OPENID_CLIENT_ID, configs: { - clientId: process.env?.SSO_OPENID_CLIENT_ID || '', - name: process.env?.SSO_OPENID_NAME || '', + clientId: envVars?.SSO_OPENID_CLIENT_ID || '', + name: envVars?.SSO_OPENID_NAME || '', clientSecret: - (process.env?.SSO_OPENID_CLIENT_SECRET && + (envVars?.SSO_OPENID_CLIENT_SECRET && (await encryptionService.encryptColumnValue( 'ssoConfigs', 'clientSecret', - process.env.SSO_OPENID_CLIENT_SECRET + envVars.SSO_OPENID_CLIENT_SECRET ))) || '', - wellKnownUrl: process.env?.SSO_OPENID_WELL_KNOWN_URL || '', + wellKnownUrl: envVars?.SSO_OPENID_WELL_KNOWN_URL || '', }, }, { diff --git a/server/data-migrations/1710780718114-AddGracePeriodExpiryDateColumnInOrganizationLicenseTable.ts b/server/data-migrations/1710780718114-AddGracePeriodExpiryDateColumnInOrganizationLicenseTable.ts index d68ba4e155..75233be2f5 100644 --- a/server/data-migrations/1710780718114-AddGracePeriodExpiryDateColumnInOrganizationLicenseTable.ts +++ b/server/data-migrations/1710780718114-AddGracePeriodExpiryDateColumnInOrganizationLicenseTable.ts @@ -1,14 +1,6 @@ -import { getTooljetEdition } from '@helpers/utils.helper'; -import { TOOLJET_EDITIONS } from '@modules/app/constants'; import { MigrationInterface, QueryRunner } from 'typeorm'; export class AddGracePeriodExpiryDateColumnInOrganizationLicenseTable1710780718114 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - const edition: TOOLJET_EDITIONS = getTooljetEdition() as TOOLJET_EDITIONS; - // If edition is not cloud, skip this migration - if (edition !== TOOLJET_EDITIONS.Cloud) { - console.log('Migration is only restricted for cloud edition.'); - return; // Exit the migration early - } await queryRunner.query('ALTER TABLE organization_license ADD COLUMN expiry_with_grace_period TIMESTAMP'); // Update the new column with expiry_date + 14 days diff --git a/server/data-migrations/1721236971725-MoveToolJetDatabaseTablesFromPublicToTenantSchema.ts b/server/data-migrations/1721236971725-MoveToolJetDatabaseTablesFromPublicToTenantSchema.ts index 8f0b5a98ae..63a19bcfd3 100644 --- a/server/data-migrations/1721236971725-MoveToolJetDatabaseTablesFromPublicToTenantSchema.ts +++ b/server/data-migrations/1721236971725-MoveToolJetDatabaseTablesFromPublicToTenantSchema.ts @@ -16,7 +16,7 @@ import { revokeAccessToPublicSchema, grantTenantRoleToTjdbAdminRole, } from '@helpers/tooljet_db.helper'; -const crypto = require('crypto'); +import * as crypto from 'crypto'; export class MoveToolJetDatabaseTablesFromPublicToTenantSchema1721236971725 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { diff --git a/server/data-migrations/1740401100000-SetDefaultWorkspace.ts b/server/data-migrations/1740401100000-SetDefaultWorkspace.ts index c337155fd8..314625421e 100644 --- a/server/data-migrations/1740401100000-SetDefaultWorkspace.ts +++ b/server/data-migrations/1740401100000-SetDefaultWorkspace.ts @@ -23,17 +23,20 @@ export class SetDefaultWorkspace1740401100000 implements MigrationInterface { where: { slug: workspaceSlug, status: WORKSPACE_STATUS.ACTIVE }, select: ['id'], }); - if (organization){ - await queryRunner.query(` + if (organization) { + await queryRunner.query( + ` UPDATE organizations SET is_default = true WHERE slug = $1 - `, [workspaceSlug]); - return; + `, + [workspaceSlug] + ); + return; } console.log(`No active organization found with slug: ${workspaceSlug}`); } - } catch (err) { + } catch { console.log('Invalid TOOLJET_DEFAULT_WORKSPACE_URL format'); } } @@ -63,4 +66,4 @@ export class SetDefaultWorkspace1740401100000 implements MigrationInterface { SET is_default = false; `); } -} +} diff --git a/server/migrations/1738235725332-AddAutoActivatedToUsers.ts b/server/migrations/1738235725332-AddAutoActivatedToUsers.ts new file mode 100644 index 0000000000..597f2719f5 --- /dev/null +++ b/server/migrations/1738235725332-AddAutoActivatedToUsers.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddAutoActivatedToUsers1738235725332 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + 'users', + new TableColumn({ + name: 'auto_activated', + type: 'boolean', + default: false, + isNullable: false, + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('users', 'auto_activated'); + } +} diff --git a/server/scripts/create-database.ts b/server/scripts/create-database.ts index 80be547fe9..ae29ed2059 100644 --- a/server/scripts/create-database.ts +++ b/server/scripts/create-database.ts @@ -48,7 +48,7 @@ function checkCommandAvailable(command: string) { try { const options = { env: process.env } as ExecFileSyncOptions; execFileSync('which', [command], options); - } catch (error) { + } catch { throw `Error: ${command} not found. Make sure it's installed and available in the system's PATH.`; } } diff --git a/server/scripts/drop-database.ts b/server/scripts/drop-database.ts index 6c79cdbb16..70993e8dbf 100644 --- a/server/scripts/drop-database.ts +++ b/server/scripts/drop-database.ts @@ -40,7 +40,7 @@ function checkCommandAvailable(command: string) { try { const options = { env: Object.assign({}, process.env) } as ExecFileSyncOptions; execFileSync('which', [command], options); - } catch (error) { + } catch { throw `Error: ${command} not found. Make sure it's installed and available in the system's PATH.`; } } diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index f43396f0ed..fb0b25746c 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -16,7 +16,7 @@ import { } from 'typeorm'; import { App } from './app.entity'; import { GroupPermission } from './group_permission.entity'; -const bcrypt = require('bcrypt'); +import * as bcrypt from 'bcrypt'; import { OrganizationUser } from './organization_user.entity'; import { File } from './file.entity'; import { Organization } from './organization.entity'; @@ -118,6 +118,9 @@ export class User extends BaseEntity { @Column({ name: 'company_size' }) companySize: string; + @Column({ name: 'auto_activated', default: false }) + autoActivated: boolean; + @Column({ name: 'password_retry_count' }) passwordRetryCount: number; diff --git a/server/src/helpers/bootstrap.helper.ts b/server/src/helpers/bootstrap.helper.ts new file mode 100644 index 0000000000..f9111388dc --- /dev/null +++ b/server/src/helpers/bootstrap.helper.ts @@ -0,0 +1,314 @@ +import { NestExpressApplication } from '@nestjs/platform-express'; +import { ConfigService } from '@nestjs/config'; +import { bootstrap as globalAgentBootstrap } from 'global-agent'; +import { join } from 'path'; +import helmet from 'helmet'; +import * as fs from 'fs'; +import { LicenseInitService } from '@modules/licensing/interfaces/IService'; +import { TOOLJET_EDITIONS, getImportPath } from '@modules/app/constants'; +import { ILicenseUtilService } from '@modules/licensing/interfaces/IUtilService'; +import { getTooljetEdition } from '@helpers/utils.helper'; + +/** + * Creates a logger instance with a specific context + */ +export function createLogger(context: string) { + return { + log: (message: string, ...optionalParams: any[]) => { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] [${context}] ${message}`, ...optionalParams); + }, + error: (message: string, error?: any) => { + const timestamp = new Date().toISOString(); + console.error(`[${timestamp}] [${context}] ERROR: ${message}`, error); + }, + warn: (message: string, ...optionalParams: any[]) => { + const timestamp = new Date().toISOString(); + console.warn(`[${timestamp}] [${context}] WARN: ${message}`, ...optionalParams); + }, + }; +} + +/** + * Raw body buffer handler for request processing + */ +export function rawBodyBuffer(req: any, res: any, buf: Buffer, encoding: BufferEncoding) { + if (buf && buf.length) { + req.rawBody = buf.toString(encoding || 'utf8'); + } +} + +/** + * Handles licensing initialization for Enterprise Edition + */ +export async function handleLicensingInit(app: NestExpressApplication) { + const logger = createLogger('Licensing'); + const tooljetEdition = getTooljetEdition() as TOOLJET_EDITIONS; + + logger.log(`Current edition: ${tooljetEdition}`); + + if (tooljetEdition !== TOOLJET_EDITIONS.EE) { + logger.log('Skipping licensing initialization for non-EE edition'); + return; + } + + try { + logger.log('Initializing Enterprise Edition licensing...'); + const importPath = await getImportPath(false, tooljetEdition); + const { LicenseUtilService } = await import(`${importPath}/licensing/util.service`); + + const licenseInitService = app.get(LicenseInitService); + const licenseUtilService = app.get(LicenseUtilService); + + logger.log('Calling license initialization service...'); + await licenseInitService.init(); + logger.log('✅ License initialization completed'); + + logger.log('Loading license configuration...'); + const License = await import(`${importPath}/licensing/configs/License`); + const license = License.default; + + logger.log('Validating hostname and subpath...'); + licenseUtilService.validateHostnameSubpath(license.Instance()?.domains); + + const licenseInfo = license.Instance(); + logger.log(`✅ License validation completed`); + logger.log(`License valid: ${licenseInfo.isValid}`); + logger.log(`License terms: ${JSON.stringify(licenseInfo.terms)}`); + + console.log(`License valid : ${licenseInfo.isValid} License Terms : ${JSON.stringify(licenseInfo.terms)} 🚀`); + } catch (error) { + logger.error('❌ Failed to initialize licensing:', error); + throw error; + } +} + +/** + * Replaces subpath placeholders in static assets + */ +export function replaceSubpathPlaceHoldersInStaticAssets() { + const logger = createLogger('StaticAssets'); + const filesToReplaceAssetPath = ['index.html', 'runtime.js', 'main.js']; + + logger.log('Starting subpath placeholder replacement...'); + + for (const fileName of filesToReplaceAssetPath) { + try { + const file = join(__dirname, '../../../../', 'frontend/build', fileName); + logger.log(`Processing file: ${fileName}`); + + let newValue = process.env.SUB_PATH; + + if (process.env.SUB_PATH === undefined) { + newValue = fileName === 'index.html' ? '/' : ''; + logger.log(`Using default value for ${fileName}: "${newValue}"`); + } else { + logger.log(`Using SUB_PATH value for ${fileName}: "${newValue}"`); + } + + if (!fs.existsSync(file)) { + logger.warn(`File not found: ${file}`); + continue; + } + + const data = fs.readFileSync(file, { encoding: 'utf8' }); + const result = data + .replace(/__REPLACE_SUB_PATH__\/api/g, join(newValue, '/api')) + .replace(/__REPLACE_SUB_PATH__/g, newValue); + + fs.writeFileSync(file, result, { encoding: 'utf8' }); + logger.log(`✅ Successfully processed: ${fileName}`); + } catch (error) { + logger.error(`❌ Failed to process ${fileName}:`, error); + } + } + + logger.log('✅ Subpath placeholder replacement completed'); +} + +/** + * Sets up security headers including CORS and CSP + */ +export function setSecurityHeaders(app: NestExpressApplication, configService: ConfigService) { + const logger = createLogger('Security'); + logger.log('Setting up security headers...'); + + try { + const tooljetHost = configService.get('TOOLJET_HOST'); + const host = new URL(tooljetHost); + const domain = host.hostname; + + logger.log(`Configuring CORS for domain: ${domain}`); + logger.log(`CORS enabled: ${configService.get('ENABLE_CORS') === 'true'}`); + + // Enable CORS + app.enableCors({ + origin: configService.get('ENABLE_CORS') === 'true' || tooljetHost, + credentials: true, + }); + + // Get CSP whitelisted domains + const cspWhitelistedDomains = configService.get('CSP_WHITELISTED_DOMAINS')?.split(',') || []; + logger.log(`CSP whitelisted domains: ${cspWhitelistedDomains.join(', ')}`); + + // Configure Helmet + app.use( + helmet({ + contentSecurityPolicy: { + useDefaults: true, + directives: { + upgradeInsecureRequests: null, + 'img-src': ['*', 'data:', 'blob:'], + 'script-src': [ + 'maps.googleapis.com', + 'storage.googleapis.com', + 'apis.google.com', + 'accounts.google.com', + "'self'", + "'unsafe-inline'", + "'unsafe-eval'", + 'blob:', + 'https://unpkg.com/@babel/standalone@7.17.9/babel.min.js', + 'https://unpkg.com/react@16.7.0/umd/react.production.min.js', + 'https://unpkg.com/react-dom@16.7.0/umd/react-dom.production.min.js', + 'cdn.skypack.dev', + 'cdn.jsdelivr.net', + 'https://esm.sh', + 'www.googletagmanager.com', + ].concat(cspWhitelistedDomains), + 'object-src': ["'self'", 'data:'], + 'media-src': ["'self'", 'data:'], + 'default-src': [ + 'maps.googleapis.com', + 'storage.googleapis.com', + 'apis.google.com', + 'accounts.google.com', + '*.sentry.io', + "'self'", + 'blob:', + 'www.googletagmanager.com', + ].concat(cspWhitelistedDomains), + 'connect-src': ['ws://' + domain, "'self'", '*', 'data:'], + 'frame-ancestors': ['*'], + 'frame-src': ['*'], + }, + }, + frameguard: configService.get('DISABLE_APP_EMBED') !== 'true' ? false : { action: 'deny' }, + hidePoweredBy: true, + referrerPolicy: { + policy: 'no-referrer', + }, + }) + ); + + logger.log(`Frame embedding ${configService.get('DISABLE_APP_EMBED') !== 'true' ? 'enabled' : 'disabled'}`); + + const subPath = configService.get('SUB_PATH'); + + // Custom headers middleware + app.use((req, res, next) => { + res.setHeader('Permissions-Policy', 'geolocation=(self), camera=(), microphone=()'); + res.setHeader('X-Powered-By', 'ToolJet'); + + if (req.path.startsWith(`${subPath || '/'}api/`)) { + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + } else { + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); + } + + return next(); + }); + + logger.log('✅ Security headers configured successfully'); + } catch (error) { + logger.error('❌ Failed to configure security headers:', error); + throw error; + } +} + +/** + * Builds the application version string + */ +export function buildVersion(): string { + const logger = createLogger('Version'); + + try { + logger.log('Reading version from .version file...'); + const rawVersion = fs.readFileSync('./.version', 'utf8').trim(); + logger.log(`Raw version: ${rawVersion}`); + + const ltsRegex = /-lts$/i; + const edition = getTooljetEdition(); + + let version: string; + if (ltsRegex.test(rawVersion)) { + // Extract base version (everything before -lts) + const baseVersion = rawVersion.replace(ltsRegex, ''); + // Construct: baseVersion-edition-lts + version = `${baseVersion}-${edition}-lts`; + logger.log(`LTS version detected. Built version: ${version}`); + } else { + // Current implementation: version-edition + version = `${rawVersion}-${edition}`; + logger.log(`Standard version. Built version: ${version}`); + } + + return version; + } catch (error) { + logger.error('❌ Failed to build version:', error); + throw error; + } +} + +/** + * Sets up global agent for HTTP proxy if configured + */ +export function setupGlobalAgent() { + const logger = createLogger('GlobalAgent'); + + if (process.env.TOOLJET_HTTP_PROXY) { + logger.log(`Setting up global HTTP proxy: ${process.env.TOOLJET_HTTP_PROXY}`); + process.env['GLOBAL_AGENT_HTTP_PROXY'] = process.env.TOOLJET_HTTP_PROXY; + globalAgentBootstrap(); + logger.log('✅ Global HTTP proxy configured'); + } else { + logger.log('No HTTP proxy configured'); + } +} + +/** + * Logs startup information + */ +export function logStartupInfo(configService: ConfigService, logger: any) { + const tooljetHost = configService.get('TOOLJET_HOST'); + const subPath = configService.get('SUB_PATH'); + const corsEnabled = configService.get('ENABLE_CORS') === 'true'; + const edition = getTooljetEdition(); + const version = globalThis.TOOLJET_VERSION; + + logger.log('='.repeat(60)); + logger.log('🚀 TOOLJET APPLICATION STARTED SUCCESSFULLY'); + logger.log('='.repeat(60)); + logger.log(`Edition: ${edition}`); + logger.log(`Version: ${version}`); + logger.log(`Host: ${tooljetHost}${subPath || ''}`); + logger.log(`Subpath: ${subPath || 'None'}`); + logger.log(`CSP Whitelisted Domains: ${configService.get('CSP_WHITELISTED_DOMAINS') || 'None'}`); + logger.log(`CORS Enabled: ${corsEnabled}`); + logger.log(`global HTTP proxy: ${configService.get('TOOLJET_HTTP_PROXY') || 'Not configured'}`); + logger.log(`Frame embedding: ${configService.get('DISABLE_APP_EMBED') !== 'true' ? 'enabled' : 'disabled'}`); + logger.log(`Environment: ${configService.get('NODE_ENV') || 'development'}`); + logger.log(`Port: ${configService.get('PORT') || 3000}`); + logger.log(`Listen Address: ${configService.get('LISTEN_ADDR') || '::'}`); + logger.log('='.repeat(60)); +} + +/** + * Logs shutdown information + */ +export function logShutdownInfo(signal: string, logger: any) { + logger.log('='.repeat(60)); + logger.log(`🛑 ${signal} SIGNAL RECEIVED - SHUTTING DOWN`); + logger.log('='.repeat(60)); + logger.log('Gracefully closing application...'); +} diff --git a/server/src/helpers/import_export.helpers.ts b/server/src/helpers/import_export.helpers.ts index 1f1bc57b7e..e9163c054d 100644 --- a/server/src/helpers/import_export.helpers.ts +++ b/server/src/helpers/import_export.helpers.ts @@ -130,7 +130,7 @@ export function extractAndReplaceReferencesFromString(input, componentIdNameMapp valueWithId: `{{${replacedExpression}}}`, valueWithBrackets: `{{${bracketNotationExpression}}}`, }); - } catch (error) { + } catch { replacedString += fullMatch; bracketNotationString += fullMatch; results.push({ @@ -207,7 +207,7 @@ export function extractAndReplaceReferencesFromString(input, componentIdNameMapp valueWithId: `{{${replacedExpression}}}`, valueWithBrackets: `{{${bracketNotationExpression}}}`, }); - } catch (error) { + } catch { replacedString += fullMatch; bracketNotationString += fullMatch; results.push({ @@ -317,7 +317,7 @@ function replaceIdsInExpression( } return result; - } catch (error) { + } catch { return expression; } } diff --git a/server/src/helpers/utils.helper.ts b/server/src/helpers/utils.helper.ts index 78e7259770..7effd864f5 100644 --- a/server/src/helpers/utils.helper.ts +++ b/server/src/helpers/utils.helper.ts @@ -7,8 +7,7 @@ import { ConflictException } from '@nestjs/common'; import { DataBaseConstraints } from './db_constraints.constants'; import { getEnvVars } from 'scripts/database-config-utils'; import { decamelizeKeys } from 'humps'; - -const semver = require('semver'); +import * as semver from 'semver'; export function parseJson(jsonString: string, errorMessage?: string): object { try { @@ -84,7 +83,7 @@ export function isJSONString(value: string): boolean { try { JSON.parse(value); return true; - } catch (e) { + } catch { return false; } } diff --git a/server/src/main.ts b/server/src/main.ts index d87e1f41ac..7c9979a191 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -14,17 +14,12 @@ import { INestApplicationContext, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { bootstrap as globalAgentBootstrap } from 'global-agent'; import { custom } from 'openid-client'; import { join } from 'path'; -import helmet from 'helmet'; import * as express from 'express'; -import * as fs from 'fs'; -import { LicenseInitService } from '@modules/licensing/interfaces/IService'; import { AppModule } from '@modules/app/module'; import { TOOLJET_EDITIONS, getImportPath } from '@modules/app/constants'; import { GuardValidator } from '@modules/app/validators/feature-guard.validator'; -import { ILicenseUtilService } from '@modules/licensing/interfaces/IUtilService'; import { ITemporalService } from '@modules/workflows/interfaces/ITemporalService'; import { getTooljetEdition } from '@helpers/utils.helper'; import { validateEdition } from '@helpers/edition.helper'; @@ -32,168 +27,152 @@ import { ResponseInterceptor } from '@modules/app/interceptors/response.intercep import { Reflector } from '@nestjs/core'; import { EventEmitter2 } from '@nestjs/event-emitter'; +// Import helper functions +import { + handleLicensingInit, + replaceSubpathPlaceHoldersInStaticAssets, + setSecurityHeaders, + buildVersion, + rawBodyBuffer, + setupGlobalAgent, + createLogger, + logStartupInfo, + logShutdownInfo, +} from '@helpers/bootstrap.helper'; + let appContext: INestApplicationContext = undefined; -async function handleLicensingInit(app: NestExpressApplication) { - const tooljetEdition = getTooljetEdition() as TOOLJET_EDITIONS; - - if (tooljetEdition !== TOOLJET_EDITIONS.EE) { - // If the edition is not EE, we don't need to initialize licensing - return; - } - - const importPath = await getImportPath(false, tooljetEdition); - const { LicenseUtilService } = await import(`${importPath}/licensing/util.service`); - - const licenseInitService = app.get(LicenseInitService); - const licenseUtilService = app.get(LicenseUtilService); - - await licenseInitService.init(); - - const License = await import(`${importPath}/licensing/configs/License`); - const license = License.default; - licenseUtilService.validateHostnameSubpath(license.Instance()?.domains); - - console.log( - `License valid : ${license.Instance().isValid} License Terms : ${JSON.stringify(license.Instance().terms)} 🚀` - ); -} -function replaceSubpathPlaceHoldersInStaticAssets() { - const filesToReplaceAssetPath = ['index.html', 'runtime.js', 'main.js']; - - for (const fileName of filesToReplaceAssetPath) { - const file = join(__dirname, '../../../', 'frontend/build', fileName); - - let newValue = process.env.SUB_PATH; - - if (process.env.SUB_PATH === undefined) { - newValue = fileName === 'index.html' ? '/' : ''; - } - - const data = fs.readFileSync(file, { encoding: 'utf8' }); - - const result = data - .replace(/__REPLACE_SUB_PATH__\/api/g, join(newValue, '/api')) - .replace(/__REPLACE_SUB_PATH__/g, newValue); - - fs.writeFileSync(file, result, { encoding: 'utf8' }); - } -} - -function setSecurityHeaders(app, configService) { - const tooljetHost = configService.get('TOOLJET_HOST'); - const host = new URL(tooljetHost); - const domain = host.hostname; - - app.enableCors({ - origin: configService.get('ENABLE_CORS') === 'true' || tooljetHost, - credentials: true, - }); - - app.use( - helmet({ - contentSecurityPolicy: { - useDefaults: true, - directives: { - upgradeInsecureRequests: null, - 'img-src': ['*', 'data:', 'blob:'], - 'script-src': [ - 'maps.googleapis.com', - 'storage.googleapis.com', - 'apis.google.com', - 'accounts.google.com', - "'self'", - "'unsafe-inline'", - "'unsafe-eval'", - 'blob:', - 'https://unpkg.com/@babel/standalone@7.17.9/babel.min.js', - 'https://unpkg.com/react@16.7.0/umd/react.production.min.js', - 'https://unpkg.com/react-dom@16.7.0/umd/react-dom.production.min.js', - 'cdn.skypack.dev', - 'cdn.jsdelivr.net', - 'https://esm.sh', - 'www.googletagmanager.com', - ].concat(configService.get('CSP_WHITELISTED_DOMAINS')?.split(',') || []), - 'object-src': ["'self'", 'data:'], - 'media-src': ["'self'", 'data:'], - 'default-src': [ - 'maps.googleapis.com', - 'storage.googleapis.com', - 'apis.google.com', - 'accounts.google.com', - '*.sentry.io', - "'self'", - 'blob:', - 'www.googletagmanager.com', - ].concat(configService.get('CSP_WHITELISTED_DOMAINS')?.split(',') || []), - 'connect-src': ['ws://' + domain, "'self'", '*', 'data:'], - 'frame-ancestors': ['*'], - 'frame-src': ['*'], - }, - }, - frameguard: configService.get('DISABLE_APP_EMBED') !== 'true' ? false : { action: 'deny' }, - hidePoweredBy: true, - referrerPolicy: { - policy: 'no-referrer', - }, - }) - ); - - app.use((req, res, next) => { - res.setHeader('Permissions-Policy', 'geolocation=(self), camera=(), microphone=()'); - res.setHeader('X-Powered-By', 'ToolJet'); - - if (req.path.startsWith('/api/')) { - res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); - } else { - res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); - } - - return next(); - }); -} - async function bootstrap() { - const app = await NestFactory.create(await AppModule.register({ IS_GET_CONTEXT: false }), { - bufferLogs: true, - abortOnError: false, - }); + const logger = createLogger('Bootstrap'); + logger.log('🚀 Starting ToolJet application bootstrap...'); - // Get DataSource from the app - await validateEdition(app); + try { + logger.log('Creating NestJS application...'); + const app = await NestFactory.create(await AppModule.register({ IS_GET_CONTEXT: false }), { + bufferLogs: true, + abortOnError: false, + }); - globalThis.TOOLJET_VERSION = `${fs.readFileSync('./.version', 'utf8').trim()}-${getTooljetEdition()}`; - process.env['RELEASE_VERSION'] = globalThis.TOOLJET_VERSION; + const configService = app.get(ConfigService); + logger.log('✅ NestJS application created successfully'); - process.on('SIGINT', async () => { - console.log('SIGINT signal received: closing application...'); - await app.close(); - process.exit(0); - }); + // Validate edition + logger.log('Validating ToolJet edition...'); + await validateEdition(app); + logger.log('✅ Edition validation completed'); - if (process.env.SERVE_CLIENT !== 'false' && process.env.NODE_ENV === 'production') { - replaceSubpathPlaceHoldersInStaticAssets(); - } - console.log(getTooljetEdition(), 'ToolJet Edition 🚀'); + // Build version + logger.log('Building version information...'); + const version = buildVersion(); + globalThis.TOOLJET_VERSION = version; + process.env['RELEASE_VERSION'] = version; + logger.log(`✅ Version set: ${version}`); - if (getTooljetEdition() !== TOOLJET_EDITIONS.Cloud) { + // Setup graceful shutdown + logger.log('Setting up graceful shutdown handlers...'); + setupGracefulShutdown(app, logger); + logger.log('✅ Graceful shutdown handlers configured'); + + // Handle static assets in production + if (process.env.SERVE_CLIENT !== 'false' && process.env.NODE_ENV === 'production') { + logger.log('Replacing subpath placeholders in static assets...'); + replaceSubpathPlaceHoldersInStaticAssets(); + logger.log('✅ Static assets processed'); + } + + // Initialize licensing + logger.log('Initializing licensing...'); await handleLicensingInit(app); + logger.log('✅ Licensing initialization completed'); + + // Configure OIDC timeout + logger.log('Configuring OIDC connection timeout...'); + const oidcTimeout = parseInt(process.env.OIDC_CONNECTION_TIMEOUT || '3500'); + custom.setHttpOptionsDefaults({ timeout: oidcTimeout }); + logger.log(`✅ OIDC timeout set to ${oidcTimeout}ms`); + + // Setup application middleware and pipes + logger.log('Setting up application middleware and pipes...'); + setupApplicationMiddleware(app); + logger.log('✅ Application middleware configured'); + + // Configure URL prefix and excluded paths + logger.log('Configuring URL prefix and excluded paths...'); + const { urlPrefix, pathsToExclude } = configureUrlPrefix(); + app.setGlobalPrefix(urlPrefix + 'api', { exclude: pathsToExclude }); + logger.log(`✅ URL prefix configured: ${urlPrefix}`); + + // Setup body parsers + logger.log('Setting up body parsers...'); + setupBodyParsers(app, configService); + logger.log('✅ Body parsers configured'); + + // Enable versioning + logger.log('Enabling API versioning...'); + app.enableVersioning({ + type: VersioningType.URI, + defaultVersion: VERSION_NEUTRAL, + }); + logger.log('✅ API versioning enabled'); + + // Setup security headers + logger.log('Setting up security headers...'); + setSecurityHeaders(app, configService); + logger.log('✅ Security headers configured'); + + // Setup static assets + logger.log('Setting up static assets...'); + app.use(`${urlPrefix}/assets`, express.static(join(__dirname, '/assets'))); + logger.log('✅ Static assets configured'); + + // Validate JWT guard + logger.log('Validating Ability guard on controllers...'); + const guardValidator = app.get(GuardValidator); + await guardValidator.validateJwtGuard(); + logger.log('✅ Ability guard validation completed'); + + // Start server + const listen_addr = process.env.LISTEN_ADDR || '::'; + const port = parseInt(process.env.PORT) || 3000; + + logger.log(`Starting server on ${listen_addr}:${port}...`); + await app.listen(port, listen_addr, async function () { + logStartupInfo(configService, logger); + }); + } catch (error) { + logger.error('❌ Failed to bootstrap application:', error); + process.exit(1); } +} - const configService = app.get(ConfigService); +function setupGracefulShutdown(app: NestExpressApplication, logger: any) { + const gracefulShutdown = async (signal: string) => { + logShutdownInfo(signal, logger); + try { + await app.close(); + logger.log('✅ Application closed successfully'); + process.exit(0); + } catch (error) { + logger.error('❌ Error during application shutdown:', error); + process.exit(1); + } + }; - custom.setHttpOptionsDefaults({ - timeout: parseInt(process.env.OIDC_CONNECTION_TIMEOUT || '3500'), // Default 3.5 seconds - }); + process.on('SIGINT', () => gracefulShutdown('SIGINT')); + process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); +} +function setupApplicationMiddleware(app: NestExpressApplication) { app.useLogger(app.get(Logger)); app.useGlobalInterceptors(new ResponseInterceptor(app.get(Reflector), app.get(Logger), app.get(EventEmitter2))); app.useGlobalFilters(new AllExceptionsFilter(app.get(Logger))); app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); app.useWebSocketAdapter(new WsAdapter(app)); +} +function configureUrlPrefix() { const hasSubPath = process.env.SUB_PATH !== undefined; - const UrlPrefix = hasSubPath ? process.env.SUB_PATH : ''; + const urlPrefix = hasSubPath ? process.env.SUB_PATH : ''; // Exclude these endpoints from prefix. These endpoints are required for health checks. const pathsToExclude = []; @@ -203,69 +182,84 @@ async function bootstrap() { pathsToExclude.push({ path: '/health', method: RequestMethod.GET }); pathsToExclude.push({ path: '/api/health', method: RequestMethod.GET }); - app.setGlobalPrefix(UrlPrefix + 'api', { - exclude: pathsToExclude, - }); + return { urlPrefix, pathsToExclude }; +} + +function setupBodyParsers(app: NestExpressApplication, configService: ConfigService) { + const maxSize = configService.get('MAX_JSON_SIZE') || '50mb'; app.use(compression()); app.use(cookieParser()); - app.use(json({ limit: '50mb' })); - app.use(urlencoded({ extended: true, limit: '50mb', parameterLimit: 1000000 })); - - app.enableVersioning({ - type: VersioningType.URI, - defaultVersion: VERSION_NEUTRAL, - }); - - setSecurityHeaders(app, configService); - - app.use(`${UrlPrefix}/assets`, express.static(join(__dirname, '/assets'))); - - const listen_addr = process.env.LISTEN_ADDR || '::'; - const port = parseInt(process.env.PORT) || 3000; - - const guardValidator = app.get(GuardValidator); - // Run the validation - await guardValidator.validateJwtGuard(); - - await app.listen(port, listen_addr, async function () { - const tooljetHost = configService.get('TOOLJET_HOST'); - const subPath = configService.get('SUB_PATH'); - - console.log(`Ready to use at ${tooljetHost}${subPath || ''} 🚀`); - }); -} - -// Bootstrap global agent only if TOOLJET_HTTP_PROXY is set -if (process.env.TOOLJET_HTTP_PROXY) { - process.env['GLOBAL_AGENT_HTTP_PROXY'] = process.env.TOOLJET_HTTP_PROXY; - globalAgentBootstrap(); + app.use(json({ verify: rawBodyBuffer, limit: maxSize })); + app.use( + urlencoded({ + verify: rawBodyBuffer, + extended: true, + limit: maxSize, + parameterLimit: 1000000, + }) + ); } async function bootstrapWorker() { - appContext = await NestFactory.createApplicationContext(await AppModule.register({ IS_GET_CONTEXT: false })); + const logger = createLogger('Worker'); + logger.log('🚀 Starting ToolJet worker bootstrap...'); - process.on('SIGINT', async () => { - console.log('SIGINT signal received: closing application...'); - temporalService.shutDownWorker(); - }); + try { + logger.log('Creating application context...'); + appContext = await NestFactory.createApplicationContext(await AppModule.register({ IS_GET_CONTEXT: false })); + logger.log('✅ Application context created'); - process.on('SIGTERM', async () => { - console.log('SIGTERM signal received: closing application...'); - temporalService.shutDownWorker(); - }); + // Setup graceful shutdown for worker + setupWorkerGracefulShutdown(logger); - const importPath = await getImportPath(false); - const { TemporalService } = await import(`${importPath}/workflows/services/temporal.service`); + logger.log('Initializing Temporal service...'); + const importPath = await getImportPath(false); + const { TemporalService } = await import(`${importPath}/workflows/services/temporal.service`); - const temporalService = appContext.get(TemporalService); - await temporalService.runWorker(); - await appContext.close(); + const temporalService = appContext.get(TemporalService); + logger.log('✅ Temporal service initialized'); + + logger.log('Starting Temporal worker...'); + await temporalService.runWorker(); + logger.log('✅ Temporal worker started'); + + await appContext.close(); + logger.log('✅ Worker bootstrap completed'); + } catch (error) { + logger.error('❌ Failed to bootstrap worker:', error); + process.exit(1); + } +} + +function setupWorkerGracefulShutdown(logger: any) { + const gracefulShutdown = async (signal: string) => { + logShutdownInfo(signal, logger); + try { + const importPath = await getImportPath(false); + const { TemporalService } = await import(`${importPath}/workflows/services/temporal.service`); + const temporalService = appContext.get(TemporalService); + + logger.log('Shutting down Temporal worker...'); + temporalService.shutDownWorker(); + logger.log('✅ Temporal worker shutdown completed'); + } catch (error) { + logger.error('❌ Error during worker shutdown:', error); + } + }; + + process.on('SIGINT', () => gracefulShutdown('SIGINT')); + process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); } export function getAppContext(): INestApplicationContext { return appContext; } + +// Bootstrap global agent only if TOOLJET_HTTP_PROXY is set +setupGlobalAgent(); + +// Main execution if (getTooljetEdition() === TOOLJET_EDITIONS.EE) { // eslint-disable-next-line @typescript-eslint/no-unused-expressions process.env.WORKER ? bootstrapWorker() : bootstrap(); diff --git a/server/src/modules/app-git/ability/guard.ts b/server/src/modules/app-git/ability/guard.ts index 41c7bf9ed9..286577b528 100644 --- a/server/src/modules/app-git/ability/guard.ts +++ b/server/src/modules/app-git/ability/guard.ts @@ -1,23 +1,21 @@ import { Injectable } from '@nestjs/common'; -import { AppGitAbilityFactory } from '.'; +import { FeatureAbilityFactory } from '.'; import { AbilityGuard } from '@modules/app/guards/ability.guard'; -import { App } from '@entities/app.entity'; import { ResourceDetails } from '@modules/app/types'; import { MODULES } from '@modules/app/constants/modules'; +import { AppGitSync } from '@entities/app_git_sync.entity'; @Injectable() -export class AppGitAbilityGuard extends AbilityGuard { - protected getResource(): ResourceDetails { - return { - resourceType: MODULES.APP_GIT, - }; +export class FeatureAbilityGuard extends AbilityGuard { + protected getResource(): ResourceDetails | ResourceDetails[] { + return [{ resourceType: MODULES.APP_GIT }, { resourceType: MODULES.APP }]; } protected getAbilityFactory() { - return AppGitAbilityFactory; + return FeatureAbilityFactory; } protected getSubjectType() { - return App; + return AppGitSync; } } diff --git a/server/src/modules/app-git/ability/index.ts b/server/src/modules/app-git/ability/index.ts index 8996f754b7..4d3dc8d5f0 100644 --- a/server/src/modules/app-git/ability/index.ts +++ b/server/src/modules/app-git/ability/index.ts @@ -3,15 +3,16 @@ import { Ability, AbilityBuilder, InferSubjects } from '@casl/ability'; import { AbilityFactory } from '@modules/app/ability-factory'; import { UserAllPermissions } from '@modules/app/types'; import { FEATURE_KEY } from '../constants'; -import { App } from '@entities/app.entity'; +import { AppGitSync } from '@entities/app_git_sync.entity'; +import { MODULES } from '@modules/app/constants/modules'; -type Subjects = InferSubjects | 'all'; +type Subjects = InferSubjects | 'all'; export type AppGitAbility = Ability<[FEATURE_KEY, Subjects]>; @Injectable() -export class AppGitAbilityFactory extends AbilityFactory { +export class FeatureAbilityFactory extends AbilityFactory { protected getSubjectType() { - return App; + return AppGitSync; } protected defineAbilityFor( @@ -21,48 +22,44 @@ export class AppGitAbilityFactory extends AbilityFactory request?: any ): void { const appId = request?.tj_resource_id; - const { superAdmin, isAdmin, isBuilder, userPermission } = UserAllPermissions; + const { superAdmin, isAdmin, userPermission } = UserAllPermissions; - const userAppGitPermissions = userPermission?.APP; + const userAppGitPermissions = userPermission?.[MODULES.APP]; const isAllAppsEditable = !!userAppGitPermissions?.isAllEditable; const isAllAppsCreatable = !!userPermission?.appCreate; - const isAllAppsViewable = !!userAppGitPermissions?.isAllViewable; - // Grant feature-level access based on resource actions - if (isAdmin || superAdmin || isBuilder) { - // Admin or Super Admin gets full access to all features - can(FEATURE_KEY.GIT_CREATE_APP, App); - can(FEATURE_KEY.GIT_UPDATE_APP, App); - can(FEATURE_KEY.GIT_GET_APPS, App); - can(FEATURE_KEY.GIT_GET_APP, App); - can(FEATURE_KEY.GIT_GET_APP_CONFIG, App); - can(FEATURE_KEY.GIT_SYNC_APP, App); - can(FEATURE_KEY.GIT_APP_VERSION_RENAME, App); - can(FEATURE_KEY.GIT_APP_CONFIGS_UPDATE, App); - can(FEATURE_KEY.GIT_FETCH_APP_CONFIGS, App); - return; - } - // READ-based features - if ( - isAllAppsViewable || - (userAppGitPermissions?.viewableAppsId?.length && appId && userAppGitPermissions?.viewableAppsId?.includes(appId)) - ) { - can(FEATURE_KEY.GIT_GET_APPS, App); - can(FEATURE_KEY.GIT_GET_APP, App); + // Used for public endpoint to get the app configs + can(FEATURE_KEY.GIT_FETCH_APP_CONFIGS, AppGitSync); + + // Grant feature-level access based on resource actions + if (isAdmin || superAdmin) { + // Admin or Super Admin gets full access to all features + can(FEATURE_KEY.GIT_CREATE_APP, AppGitSync); + can(FEATURE_KEY.GIT_UPDATE_APP, AppGitSync); + can(FEATURE_KEY.GIT_GET_APPS, AppGitSync); + can(FEATURE_KEY.GIT_GET_APP, AppGitSync); + can(FEATURE_KEY.GIT_GET_APP_CONFIG, AppGitSync); + can(FEATURE_KEY.GIT_SYNC_APP, AppGitSync); + can(FEATURE_KEY.GIT_APP_VERSION_RENAME, AppGitSync); + can(FEATURE_KEY.GIT_APP_CONFIGS_UPDATE, AppGitSync); + return; } // CREATE-based features if (isAllAppsCreatable) { - can(FEATURE_KEY.GIT_CREATE_APP, App); + can(FEATURE_KEY.GIT_CREATE_APP, AppGitSync); + can(FEATURE_KEY.GIT_GET_APPS, AppGitSync); } - - // UPDATE-based features if ( isAllAppsEditable || (userAppGitPermissions?.editableAppsId?.length && appId && userAppGitPermissions.editableAppsId.includes(appId)) ) { - can(FEATURE_KEY.GIT_UPDATE_APP, App); - can(FEATURE_KEY.GIT_SYNC_APP, App); + can(FEATURE_KEY.GIT_UPDATE_APP, AppGitSync); + can(FEATURE_KEY.GIT_SYNC_APP, AppGitSync); + can(FEATURE_KEY.GIT_APP_VERSION_RENAME, AppGitSync); + can(FEATURE_KEY.GIT_APP_CONFIGS_UPDATE, AppGitSync); + can(FEATURE_KEY.GIT_GET_APP, AppGitSync); // Used for syncing data from inside the application so only users with edit permission can perform the operation + can(FEATURE_KEY.GIT_GET_APP_CONFIG, AppGitSync); } // Additional checks based on specific actions @@ -71,7 +68,7 @@ export class AppGitAbilityFactory extends AbilityFactory appId && userAppGitPermissions.editableAppsId.includes(appId) ) { - can(FEATURE_KEY.GIT_GET_APP_CONFIG, App); + can(FEATURE_KEY.GIT_GET_APP_CONFIG, AppGitSync); } } } diff --git a/server/src/modules/app-git/constants/feature.ts b/server/src/modules/app-git/constants/feature.ts index 5c5cada809..75cb30255e 100644 --- a/server/src/modules/app-git/constants/feature.ts +++ b/server/src/modules/app-git/constants/feature.ts @@ -5,14 +5,14 @@ import { LICENSE_FIELD } from '@modules/licensing/constants'; export const FEATURES: FeaturesConfig = { [MODULES.APP_GIT]: { - [FEATURE_KEY.GIT_CREATE_APP]: { license: LICENSE_FIELD.VALID }, - [FEATURE_KEY.GIT_GET_APP]: { license: LICENSE_FIELD.VALID }, - [FEATURE_KEY.GIT_GET_APPS]: { license: LICENSE_FIELD.VALID }, - [FEATURE_KEY.GIT_GET_APP_CONFIG]: { license: LICENSE_FIELD.VALID }, - [FEATURE_KEY.GIT_SYNC_APP]: { license: LICENSE_FIELD.VALID }, - [FEATURE_KEY.GIT_UPDATE_APP]: { license: LICENSE_FIELD.VALID }, - [FEATURE_KEY.GIT_APP_VERSION_RENAME]: { license: LICENSE_FIELD.VALID }, - [FEATURE_KEY.GIT_APP_CONFIGS_UPDATE]: { license: LICENSE_FIELD.VALID }, - [FEATURE_KEY.GIT_FETCH_APP_CONFIGS]: { isPublic: true }, + [FEATURE_KEY.GIT_CREATE_APP]: { license: LICENSE_FIELD.GIT_SYNC }, // Used for importing an application from git to any workspace + [FEATURE_KEY.GIT_GET_APP]: { license: LICENSE_FIELD.GIT_SYNC }, // Used to fetch the latest git commit data for syncing the application + [FEATURE_KEY.GIT_GET_APPS]: { license: LICENSE_FIELD.GIT_SYNC }, // Used for listing all the application from GIT + [FEATURE_KEY.GIT_GET_APP_CONFIG]: { license: LICENSE_FIELD.GIT_SYNC }, // Used for getting latest app configs and creates an application if app is not already created + [FEATURE_KEY.GIT_SYNC_APP]: { license: LICENSE_FIELD.GIT_SYNC }, // Push an application to git + [FEATURE_KEY.GIT_UPDATE_APP]: { license: LICENSE_FIELD.GIT_SYNC }, // Update the application with latest git commit + [FEATURE_KEY.GIT_APP_VERSION_RENAME]: { license: LICENSE_FIELD.GIT_SYNC }, // Rename app/version name + [FEATURE_KEY.GIT_APP_CONFIGS_UPDATE]: { license: LICENSE_FIELD.GIT_SYNC }, // Used to update the permission to allow app edit for imported applications + [FEATURE_KEY.GIT_FETCH_APP_CONFIGS]: {}, // Used for fetching app configs }, }; diff --git a/server/src/modules/app-git/controller.ts b/server/src/modules/app-git/controller.ts index df68e38551..60d058b531 100644 --- a/server/src/modules/app-git/controller.ts +++ b/server/src/modules/app-git/controller.ts @@ -10,9 +10,7 @@ import { } from '@modules/app-git/dto'; import { MODULES } from '@modules/app/constants/modules'; import { InitModule } from '@modules/app/decorators/init-module'; -import { LICENSE_FIELD } from '@modules/licensing/constants'; import { InitFeature } from '@modules/app/decorators/init-feature.decorator'; -import { RequireFeature } from '@modules/app/decorators/require-feature.decorator'; import { FEATURE_KEY } from './constants'; @InitModule(MODULES.APP_GIT) @@ -20,7 +18,6 @@ import { FEATURE_KEY } from './constants'; export class AppGitController { constructor() {} - @RequireFeature(LICENSE_FIELD.GIT_SYNC) @InitFeature(FEATURE_KEY.GIT_GET_APPS) @UseGuards(JwtAuthGuard) @Get('gitpull') @@ -28,7 +25,6 @@ export class AppGitController { throw new NotFoundException(); } - @RequireFeature(LICENSE_FIELD.GIT_SYNC) @InitFeature(FEATURE_KEY.GIT_SYNC_APP) @UseGuards(JwtAuthGuard) @Post('gitpush/:appGitId/:versionId') @@ -40,7 +36,6 @@ export class AppGitController { throw new NotFoundException(); } - @RequireFeature(LICENSE_FIELD.GIT_SYNC) @InitFeature(FEATURE_KEY.GIT_GET_APP) @UseGuards(JwtAuthGuard) @Get('gitpull/app/:appId') @@ -48,7 +43,6 @@ export class AppGitController { throw new NotFoundException(); } - @RequireFeature(LICENSE_FIELD.GIT_SYNC) @InitFeature(FEATURE_KEY.GIT_GET_APP_CONFIG) @UseGuards(JwtAuthGuard) @Get(':workspaceId/app/:versionId') @@ -60,7 +54,6 @@ export class AppGitController { throw new NotFoundException(); } - @RequireFeature(LICENSE_FIELD.GIT_SYNC) @InitFeature(FEATURE_KEY.GIT_CREATE_APP) @UseGuards(JwtAuthGuard) @Post('gitpull/app') @@ -68,7 +61,6 @@ export class AppGitController { throw new NotFoundException(); } - @RequireFeature(LICENSE_FIELD.GIT_SYNC) @InitFeature(FEATURE_KEY.GIT_UPDATE_APP) @UseGuards(JwtAuthGuard) @Post('gitpull/app/:appId') @@ -76,7 +68,6 @@ export class AppGitController { throw new NotFoundException(); } - @RequireFeature(LICENSE_FIELD.GIT_SYNC) @InitFeature(FEATURE_KEY.GIT_APP_VERSION_RENAME) @Put('app/:appId/rename') async renameAppOrVersion( @@ -87,7 +78,6 @@ export class AppGitController { throw new NotFoundException(); } - @RequireFeature(LICENSE_FIELD.GIT_SYNC) @InitFeature(FEATURE_KEY.GIT_APP_CONFIGS_UPDATE) @Put(':appId/configs') async updateAppGitConfigs( @@ -98,7 +88,6 @@ export class AppGitController { throw new NotFoundException(); } - @RequireFeature(LICENSE_FIELD.GIT_SYNC) @Get(':workspaceId/app/:versionId/configs') async getAppGitConfigs( @User() user, diff --git a/server/src/modules/app-git/guards/app-resource.guard.ts b/server/src/modules/app-git/guards/app-resource.guard.ts new file mode 100644 index 0000000000..8c5d2d2b42 --- /dev/null +++ b/server/src/modules/app-git/guards/app-resource.guard.ts @@ -0,0 +1,38 @@ +import { Injectable, CanActivate, ExecutionContext, BadRequestException, NotFoundException } from '@nestjs/common'; +import { AppsRepository } from '@modules/apps/repository'; +import { User } from '@entities/user.entity'; +import { VersionRepository } from '@modules/versions/repository'; +import { App } from '@entities/app.entity'; +// This Guard should be used after jwt auth guard +@Injectable() +export class AppResourceGuard implements CanActivate { + constructor( + protected readonly appRepository: AppsRepository, + protected readonly versionRepository: VersionRepository + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { appId, versionId } = request.params; + const user: User = request.user; + if (!appId && !versionId) { + throw new BadRequestException('App ID or version ID must be provided'); + } + + let app: App; + if (appId) { + app = request.tj_app || (appId && (await this.appRepository.findById(appId, user.organizationId))); + } else if (versionId) { + const version = await this.versionRepository.getAppVersionById(versionId); + app = version?.app; + } + if (!app) { + throw new NotFoundException('App not found. Invalid App id'); + } + + // Attach the found app to the request + request.tj_app = app; + request.tj_resource_id = app.id; + return true; + } +} diff --git a/server/src/modules/app-git/module.ts b/server/src/modules/app-git/module.ts index 3178afc212..e9929ed888 100644 --- a/server/src/modules/app-git/module.ts +++ b/server/src/modules/app-git/module.ts @@ -6,7 +6,7 @@ import { AppsModule } from '@modules/apps/module'; import { TooljetDbModule } from '@modules/tooljet-db/module'; import { ImportExportResourcesModule } from '@modules/import-export-resources/module'; import { VersionModule } from '@modules/versions/module'; -import { AppGitAbilityFactory } from '@modules/app-git/ability/index'; +import { FeatureAbilityFactory } from '@modules/app-git/ability/index'; import { OrganizationGitSyncRepository } from '@modules/git-sync/repository'; import { AppGitRepository } from './repository'; import { SubModule } from '@modules/app/sub-module'; @@ -58,7 +58,7 @@ export class AppGitModule extends SubModule { HTTPSAppGitUtilityService, GitLabAppGitUtilityService, VersionRepository, - AppGitAbilityFactory, + FeatureAbilityFactory, AppVersionRenameListener, ], exports: [SSHAppGitUtilityService, HTTPSAppGitUtilityService, GitLabAppGitUtilityService], diff --git a/server/src/modules/app/constants/module-info.ts b/server/src/modules/app/constants/module-info.ts index 8d54a1226b..b1fb9fce3e 100644 --- a/server/src/modules/app/constants/module-info.ts +++ b/server/src/modules/app/constants/module-info.ts @@ -40,6 +40,7 @@ import { FEATURES as APP_PERMISSIONS_FEATURES } from '@modules/app-permissions/c import { FEATURES as EXTERNAL_API_FEATURES } from '@modules/external-apis/constants/feature'; import { FEATURES as MODULE_FEATURES } from '@modules/modules/constants/feature'; import { FEATURES as APP_GIT_FEATURES } from '@modules/app-git/constants/feature'; +import { FEATURES as GIT_SYNC_FEATURES } from '@modules/git-sync/constants/feature'; const GROUP_PERMISSIONS_FEATURES = getTooljetEdition() === TOOLJET_EDITIONS.EE ? GROUP_PERMISSIONS_FEATURES_EE : GROUP_PERMISSIONS_FEATURES_CE; @@ -85,4 +86,5 @@ export const MODULE_INFO: { [key: string]: any } = { ...EXTERNAL_API_FEATURES, ...MODULE_FEATURES, ...APP_GIT_FEATURES, + ...GIT_SYNC_FEATURES, }; diff --git a/server/src/modules/app/constants/modules.ts b/server/src/modules/app/constants/modules.ts index 4e059420db..8a1f3c4930 100644 --- a/server/src/modules/app/constants/modules.ts +++ b/server/src/modules/app/constants/modules.ts @@ -22,7 +22,6 @@ export enum MODULES { CUSTOM_STYLES = 'CustomStyles', SMTP = 'SMTP', ONBOARDING = 'Onboarding', - APP_GIT = 'AppGit', //register INSTANCE_SETTINGS = 'instanceSettings', LICENSING = 'Licensing', FILE = 'file', @@ -41,5 +40,6 @@ export enum MODULES { EXTERNAL_APIS = 'externalApis', ORGANIZATION_PAYMENTS = 'organizationPayments', MODULES = 'Modules', - GIT_SYNC = 'GitSync', //register + APP_GIT = 'AppGit', + GIT_SYNC = 'GitSync', } diff --git a/server/src/modules/app/guards/cloud-feature.guard.ts b/server/src/modules/app/guards/cloud-feature.guard.ts new file mode 100644 index 0000000000..fac40fd723 --- /dev/null +++ b/server/src/modules/app/guards/cloud-feature.guard.ts @@ -0,0 +1,10 @@ +import { getTooljetEdition } from '@helpers/utils.helper'; +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { TOOLJET_EDITIONS } from '../constants'; + +@Injectable() +export class CloudFeatureGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + return getTooljetEdition() === TOOLJET_EDITIONS.Cloud; + } +} diff --git a/server/src/modules/app/validators/feature-guard.validator.ts b/server/src/modules/app/validators/feature-guard.validator.ts index f3b76d1e5b..2c8f9aa587 100644 --- a/server/src/modules/app/validators/feature-guard.validator.ts +++ b/server/src/modules/app/validators/feature-guard.validator.ts @@ -7,9 +7,12 @@ import { GUARDS_METADATA } from '@nestjs/common/constants'; @Injectable() // Validates if all routes are guarded with AbilityGuard export class GuardValidator { - private unprotectedRoutes: string[] = []; + private unprotectedRoutes = new Set(); - constructor(private readonly metadataScanner: MetadataScanner, private readonly modulesContainer: ModulesContainer) {} + constructor( + private readonly metadataScanner: MetadataScanner, + private readonly modulesContainer: ModulesContainer + ) {} async validateJwtGuard() { console.log('Validating if all routes are guarded with AbilityGuard'); @@ -53,13 +56,13 @@ export class GuardValidator { const hasJwtGuard = this.hasJwtAuthGuard(allGuards); if (!hasJwtGuard) { - this.unprotectedRoutes.push(`${requestMethod} ${routePath}`); + this.unprotectedRoutes.add(`${requestMethod} ${routePath}`); } } } } - if (this.unprotectedRoutes.length > 0) { + if (this.unprotectedRoutes.size > 0) { console.error( '\x1b[31m%s\x1b[0m', 'ERROR: The following routes are not protected by AbilityGuard or its descendants:' @@ -105,7 +108,7 @@ export class GuardValidator { return false; } catch (error) { - console.error('Error checking guard:', guard); + console.error('Error checking guard:', guard, error); return false; } }); diff --git a/server/src/modules/apps/service.ts b/server/src/modules/apps/service.ts index 2f57c6d737..cfbc5280f6 100644 --- a/server/src/modules/apps/service.ts +++ b/server/src/modules/apps/service.ts @@ -60,7 +60,7 @@ export class AppsService implements IAppsService { protected readonly componentsService: ComponentsService, protected readonly eventEmitter: EventEmitter2, protected readonly appGitRepository: AppGitRepository - ) { } + ) {} async create(user: User, appCreateDto: AppCreateDto) { const { name, icon, type, prompt } = appCreateDto; return await dbTransactionWrap(async (manager: EntityManager) => { @@ -107,12 +107,12 @@ export class AppsService implements IAppsService { : versionName ? await this.versionRepository.findByName(versionName, app.id) : // Handle version retrieval based on env - await this.versionRepository.findLatestVersionForEnvironment( - app.id, - envId, - environmentName, - app.organizationId - ); + await this.versionRepository.findLatestVersionForEnvironment( + app.id, + envId, + environmentName, + app.organizationId + ); if (!version) { throw new NotFoundException("Couldn't found app version. Please check the version name"); @@ -332,7 +332,7 @@ export class AppsService implements IAppsService { : await this.versionRepository.findVersion(app.editingVersion?.id); const pagesForVersion = app.editingVersion - ? await this.pageService.findPagesForVersion(versionToLoad.id, user.organizationId) + ? await this.pageService.findPagesForVersion(versionToLoad.id, app.organizationId) : []; const eventsForVersion = app.editingVersion ? await this.eventService.findEventsForVersion(versionToLoad.id) : []; const appTheme = await this.organizationThemeUtilService.getTheme( diff --git a/server/src/modules/auth/constants/feature.ts b/server/src/modules/auth/constants/feature.ts index c21c4195ea..6a32030180 100644 --- a/server/src/modules/auth/constants/feature.ts +++ b/server/src/modules/auth/constants/feature.ts @@ -51,5 +51,17 @@ export const FEATURES: FeaturesConfig = { [FEATURE_KEY.OAUTH_SAML_RESPONSE]: { isPublic: true, }, + [FEATURE_KEY.AI_ONBOARDING]: { + isPublic: true, + }, + [FEATURE_KEY.AI_ONBOARDING_SSO]: { + isPublic: true, + }, + [FEATURE_KEY.AI_COOKIE_SET]: { + isPublic: true, + }, + [FEATURE_KEY.AI_COOKIE_DELETE]: { + isPublic: true, + }, }, }; diff --git a/server/src/modules/auth/constants/index.ts b/server/src/modules/auth/constants/index.ts index ef769b9230..0ddfe50e0b 100644 --- a/server/src/modules/auth/constants/index.ts +++ b/server/src/modules/auth/constants/index.ts @@ -21,4 +21,10 @@ export enum FEATURE_KEY { OAUTH_SAML_CONFIGS = '/oauth/saml/configs/:configId', OAUTH_COMMON_SIGN_IN = '/oauth/sign-in/common/:ssoType', OAUTH_SAML_RESPONSE = '/oauth/saml/:configId', + + // AI Onboarding + AI_ONBOARDING = 'aiOnboarding', // POST 'ai-onboarding' + AI_ONBOARDING_SSO = 'aiOnboardingSSO', // POST 'sign-in/common/:ssoType + AI_COOKIE_SET = 'aiCookieSet', // POST 'set-ai-cookie' + AI_COOKIE_DELETE = 'aiCookieDelete', // GET 'delete-ai-cookies' } diff --git a/server/src/modules/auth/decorators/ai-cookie.decorator.ts b/server/src/modules/auth/decorators/ai-cookie.decorator.ts new file mode 100644 index 0000000000..fc13ae57af --- /dev/null +++ b/server/src/modules/auth/decorators/ai-cookie.decorator.ts @@ -0,0 +1,13 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const AiCookies = createParamDecorator((data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + let aiCookies = {}; + if (request.cookies['tj_ai_prompt'] || request.cookies['tj_template_id']) { + aiCookies = { + tj_ai_prompt: request.cookies['tj_ai_prompt'], + tj_template_id: request.cookies['tj_template_id'], + }; + } + return aiCookies || {}; +}); diff --git a/server/src/modules/auth/dto/index.ts b/server/src/modules/auth/dto/index.ts index b166d74a75..6bb69a2cfe 100644 --- a/server/src/modules/auth/dto/index.ts +++ b/server/src/modules/auth/dto/index.ts @@ -1,5 +1,5 @@ import { IsEmail, IsNotEmpty, IsOptional, IsString, IsUUID, MaxLength, MinLength } from 'class-validator'; -import { lowercaseString } from 'src/helpers/utils.helper'; +import { lowercaseString, sanitizeInput } from 'src/helpers/utils.helper'; import { Transform } from 'class-transformer'; export class AppAuthenticationDto { @@ -74,3 +74,20 @@ export class ChangePasswordDto { @MaxLength(100, { message: 'Password should be Max 100 characters' }) newPassword: string; } + +export class CreateAiUserDto { + @IsString() + @Transform(({ value }) => sanitizeInput(value)) + name: string; + + @IsEmail() + @Transform(({ value }) => { + const newValue = sanitizeInput(value); + return lowercaseString(newValue); + }) + email: string; + + @IsString() + @MinLength(5, { message: 'Password should contain more than 5 letters' }) + password: string; +} diff --git a/server/src/modules/auth/interfaces/IController.ts b/server/src/modules/auth/interfaces/IController.ts index aa7545d49f..c7831b21b6 100644 --- a/server/src/modules/auth/interfaces/IController.ts +++ b/server/src/modules/auth/interfaces/IController.ts @@ -1,6 +1,7 @@ import { Response } from 'express'; import { User } from '@entities/user.entity'; -import { AppAuthenticationDto, AppForgotPasswordDto, AppPasswordResetDto } from '../dto'; +import { AppAuthenticationDto, AppForgotPasswordDto, AppPasswordResetDto, CreateAiUserDto } from '../dto'; +import { SSOType } from '@entities/sso_config.entity'; export interface IAuthController { login(appAuthDto: AppAuthenticationDto, response: Response): Promise; @@ -16,3 +17,39 @@ export interface IAuthController { forgotPassword(appAuthDto: AppForgotPasswordDto): Promise>; resetPassword(appAuthDto: AppPasswordResetDto): Promise>; } + +export interface IWebsiteAuthController { + /** + * Handles user onboarding process + * @param onboardingData - The user data for onboarding + * @param response - Express response object + * @returns Promise with onboarding result + */ + onboard(onboardingData: CreateAiUserDto, response: Response): Promise; + + /** + * Handles common sign-in process for SSO providers + * @param ssoType - The SSO type (Google or Git) + * @param body - Request body data + * @param user - Authenticated user object + * @param response - Express response object + * @returns Promise with sign-in result + */ + commonSignIn(ssoType: SSOType.GOOGLE | SSOType.GIT, body: any, user: any, response: Response): Promise; + + /** + * Sets AI-related cookies in response (Safari browser support) + * @param response - Express response object + * @param body - Cookie data to set + * @returns Promise with cookie setting result + */ + setAiCookie(response: Response, body: Record): any; + + /** + * Deletes AI-related cookies from response + * @param response - Express response object + * @param cookies - Current cookies to clear + * @returns Promise with cookie clearing result + */ + deleteAiCookies(response: Response, cookies: Record): any; +} diff --git a/server/src/modules/auth/interfaces/IService.ts b/server/src/modules/auth/interfaces/IService.ts index cdeafa391d..256ab68724 100644 --- a/server/src/modules/auth/interfaces/IService.ts +++ b/server/src/modules/auth/interfaces/IService.ts @@ -1,6 +1,8 @@ import { Response } from 'express'; import { User } from '@entities/user.entity'; -import { AppAuthenticationDto } from '../dto'; +import { AppAuthenticationDto, CreateAiUserDto } from '../dto'; +import { EntityManager } from 'typeorm'; +import { SSOType } from '@entities/sso_config.entity'; export interface IAuthService { login(response: Response, appAuthDto: AppAuthenticationDto, organizationId?: string, user?: User): Promise; @@ -9,3 +11,38 @@ export interface IAuthService { forgotPassword(email: string): Promise; resetPassword(token: string, password: string): Promise; } + +export interface IWebsiteAuthService { + /** + * Handles the complete onboarding process for new users + * @param userParams - User data for onboarding + * @param existingUser - Optional existing user object + * @param response - Express response object for setting cookies + * @param ssoType - Optional SSO type for social login + * @param manager - Optional database transaction manager + * @returns Promise with login result payload + */ + handleOnboarding( + userParams: CreateAiUserDto, + existingUser?: User, + response?: Response, + ssoType?: SSOType.GOOGLE | SSOType.GIT, + manager?: EntityManager + ): Promise; + + /** + * Sets AI-related session cookies in response + * @param response - Express response object + * @param keyValues - Cookie key-value pairs to set + * @returns Promise with success message + */ + setSessionAICookies(response: Response, keyValues: Record): { message: string }; + + /** + * Clears AI-related session cookies from response + * @param response - Express response object + * @param cookies - Current cookies to clear + * @returns Promise with success message + */ + clearSessionAICookies(response: Response, cookies: Record): { message: string }; +} diff --git a/server/src/modules/auth/module.ts b/server/src/modules/auth/module.ts index d5bbed351c..4c0174e0fe 100644 --- a/server/src/modules/auth/module.ts +++ b/server/src/modules/auth/module.ts @@ -4,7 +4,6 @@ import { InstanceSettingsModule } from '@modules/instance-settings/module'; import { OrganizationUsersModule } from '@modules/organization-users/module'; import { RolesModule } from '@modules/roles/module'; import { GroupPermissionsModule } from '@modules/group-permissions/module'; -import { OnboardingModule } from '@modules/onboarding/module'; import { ProfileModule } from '@modules/profile/module'; import { UserRepository } from '@modules/users/repositories/repository'; import { OrganizationRepository } from '@modules/organizations/repository'; @@ -18,6 +17,7 @@ import { SetupOrganizationsModule } from '@modules/setup-organization/module'; import { SSOConfigsRepository } from '@modules/login-configs/repository'; import { AppEnvironmentsModule } from '@modules/app-environments/module'; import { SubModule } from '@modules/app/sub-module'; +import { OnboardingModule } from '@modules/onboarding/module'; @Module({}) export class AuthModule extends SubModule { @@ -33,6 +33,8 @@ export class AuthModule extends SubModule { GoogleOAuthService, OidcOAuthService, LdapService, + WebsiteAuthController, + WebsiteAuthService, } = await this.getProviders(configs, 'auth', [ 'controller', 'service', @@ -44,6 +46,8 @@ export class AuthModule extends SubModule { 'oauth/util-services/google-oauth.service', 'oauth/util-services/oidc-auth.service', 'oauth/util-services/ldap.service', + 'website/controller', + 'website/service', ]); return { @@ -53,16 +57,15 @@ export class AuthModule extends SubModule { await InstanceSettingsModule.register(configs), await OrganizationUsersModule.register(configs), await RolesModule.register(configs), - await OnboardingModule.register(configs), await GroupPermissionsModule.register(configs), await ProfileModule.register(configs), await SessionModule.register(configs), - await OrganizationUsersModule.register(configs), await LoginConfigsModule.register(configs), await SetupOrganizationsModule.register(configs), await AppEnvironmentsModule.register(configs), + await OnboardingModule.register(configs), ], - controllers: [AuthController, OauthController], + controllers: [AuthController, OauthController, WebsiteAuthController], providers: [ AuthService, UserRepository, @@ -80,6 +83,7 @@ export class AuthModule extends SubModule { FeatureAbilityFactory, GroupPermissionsRepository, SSOConfigsRepository, + WebsiteAuthService, ], exports: [AuthUtilService], }; diff --git a/server/src/modules/auth/oauth/service.ts b/server/src/modules/auth/oauth/service.ts index 2cc1be2dea..b6b6ea2644 100644 --- a/server/src/modules/auth/oauth/service.ts +++ b/server/src/modules/auth/oauth/service.ts @@ -37,7 +37,7 @@ import { LicenseUserService } from '@modules/licensing/services/user.service'; import { OnboardingUtilService } from '@modules/onboarding/util.service'; import { SessionUtilService } from '@modules/session/util.service'; import { SetupOrganizationsUtilService } from '@modules/setup-organization/util.service'; -const uuid = require('uuid'); +import * as uuid from 'uuid'; @Injectable() export class OauthService implements IOAuthService { @@ -221,7 +221,11 @@ export class OauthService implements IOAuthService { if (!isInviteRedirect) { // no SSO login enabled organization available for user - creating new one const { name, slug } = generateNextNameAndSlug('My workspace'); - organizationDetails = await this.setupOrganizationsUtilService.create({ name, slug }, userDetails, manager); + organizationDetails = await this.setupOrganizationsUtilService.create( + { name, slug }, + userDetails, + manager + ); await this.userRepository.updateOne( userDetails.id, { defaultOrganizationId: organizationDetails.id }, @@ -283,9 +287,8 @@ export class OauthService implements IOAuthService { signupOrganizationId !== defaultOrganizationId; let personalWorkspace: Organization; if (shouldActivatePersonalWorkspace) { - const defaultOrganizationUser = await this.organizationUsersRepository.getOrganizationUser( - defaultOrganizationId - ); + const defaultOrganizationUser = + await this.organizationUsersRepository.getOrganizationUser(defaultOrganizationId); await this.organizationUsersUtilService.activateOrganization(defaultOrganizationUser, manager); } diff --git a/server/src/modules/auth/types/index.ts b/server/src/modules/auth/types/index.ts index 2283b240d6..351d1655e5 100644 --- a/server/src/modules/auth/types/index.ts +++ b/server/src/modules/auth/types/index.ts @@ -16,6 +16,10 @@ export interface Features { [FEATURE_KEY.OAUTH_SAML_CONFIGS]: FeatureConfig; [FEATURE_KEY.OAUTH_SAML_RESPONSE]: FeatureConfig; [FEATURE_KEY.OAUTH_SIGN_IN]: FeatureConfig; + [FEATURE_KEY.AI_ONBOARDING]: FeatureConfig; + [FEATURE_KEY.AI_ONBOARDING_SSO]: FeatureConfig; + [FEATURE_KEY.AI_COOKIE_SET]: FeatureConfig; + [FEATURE_KEY.AI_COOKIE_DELETE]: FeatureConfig; } export interface FeaturesConfig { diff --git a/server/src/modules/auth/website/controller.ts b/server/src/modules/auth/website/controller.ts new file mode 100644 index 0000000000..a2ead1cd78 --- /dev/null +++ b/server/src/modules/auth/website/controller.ts @@ -0,0 +1,55 @@ +import { Controller, Post, Body, Res, UseGuards, Param, Get, NotImplementedException } from '@nestjs/common'; +import { Response } from 'express'; +import { SSOType } from '@entities/sso_config.entity'; +import { IWebsiteAuthController } from '../interfaces/IController'; +import { CreateAiUserDto } from '../dto'; +import { OrganizationAuthGuard } from '@modules/session/guards/organization-auth.guard'; +import { User } from '@modules/app/decorators/user.decorator'; +import { MODULES } from '@modules/app/constants/modules'; +import { InitModule } from '@modules/app/decorators/init-module'; +import { InitFeature } from '@modules/app/decorators/init-feature.decorator'; +import { FEATURE_KEY } from '../constants'; +import { AiCookies } from '../decorators/ai-cookie.decorator'; +import { FeatureAbilityGuard } from '../ability/guard'; + +/* + This module is for ai onboarding from the website + Email and password signup and common ssos - google and git ssos will be supported +*/ +@InitModule(MODULES.AUTH) +@Controller('ai/onboarding') +export class WebsiteAuthController implements IWebsiteAuthController { + @InitFeature(FEATURE_KEY.AI_ONBOARDING) + @UseGuards(FeatureAbilityGuard) + @Post() + async onboard(@Body() onboardingData: CreateAiUserDto, @Res({ passthrough: true }) response: Response) { + throw new NotImplementedException(); + } + + @InitFeature(FEATURE_KEY.AI_ONBOARDING_SSO) + @UseGuards(OrganizationAuthGuard, FeatureAbilityGuard) + @Post('sign-in/common/:ssoType') + async commonSignIn( + @Param('ssoType') ssoType: SSOType.GOOGLE | SSOType.GIT, + @Body() body, + @User() user, + @Res({ passthrough: true }) response: Response + ) { + throw new NotImplementedException(); + } + + /* Incase if we need to support the safari browsers */ + @InitFeature(FEATURE_KEY.AI_COOKIE_SET) + @Post('set-ai-cookie') + @UseGuards(FeatureAbilityGuard) + setAiCookie(@Res({ passthrough: true }) response: Response, @Body() body: Record) { + throw new NotImplementedException(); + } + + @InitFeature(FEATURE_KEY.AI_COOKIE_DELETE) + @Get('delete-ai-cookies') + @UseGuards(FeatureAbilityGuard) + deleteAiCookies(@Res({ passthrough: true }) response: Response, @AiCookies() cookies: Record) { + throw new NotImplementedException(); + } +} diff --git a/server/src/modules/auth/website/service.ts b/server/src/modules/auth/website/service.ts new file mode 100644 index 0000000000..82acc3dd9b --- /dev/null +++ b/server/src/modules/auth/website/service.ts @@ -0,0 +1,28 @@ +import { Injectable, NotImplementedException } from '@nestjs/common'; +import { EntityManager } from 'typeorm'; +import { User } from '@entities/user.entity'; +import { Response } from 'express'; +import { IWebsiteAuthService } from '../interfaces/IService'; +import { SSOType } from '@entities/sso_config.entity'; +import { CreateAiUserDto } from '../dto'; + +@Injectable() +export class WebsiteAuthService implements IWebsiteAuthService { + async handleOnboarding( + userParams: CreateAiUserDto, + existingUser?: User, + response?: Response, + ssoType?: SSOType.GOOGLE | SSOType.GIT, + manager?: EntityManager + ) { + throw new NotImplementedException('Method not implemented'); + } + + setSessionAICookies(response: Response, keyValues: Record) { + return { message: 'AI Cookies set successfully' }; + } + + clearSessionAICookies(response: Response, cookies: Record) { + return { message: 'AI Cookies cleared successfully' }; + } +} diff --git a/server/src/modules/data-sources/module.ts b/server/src/modules/data-sources/module.ts index 38e7ad0ef7..ee312174ac 100644 --- a/server/src/modules/data-sources/module.ts +++ b/server/src/modules/data-sources/module.ts @@ -30,8 +30,6 @@ export class DataSourcesModule extends SubModule { 'services/sample-ds.service', ]); - const { OrganizationsService } = await this.getProviders(configs, 'organizations', ['service']); - return { module: DataSourcesModule, imports: [ @@ -53,7 +51,6 @@ export class DataSourcesModule extends SubModule { PluginsRepository, SampleDataSourceService, FeatureAbilityFactory, - OrganizationsService, OrganizationRepository, ], controllers: [DataSourcesController], diff --git a/server/src/modules/data-sources/service.ts b/server/src/modules/data-sources/service.ts index f2454744fd..4381017d85 100644 --- a/server/src/modules/data-sources/service.ts +++ b/server/src/modules/data-sources/service.ts @@ -20,10 +20,9 @@ import { GetQueryVariables, UpdateOptions } from './types'; import { DataSource } from '@entities/data_source.entity'; import { PluginsServiceSelector } from './services/plugin-selector.service'; import { IDataSourcesService } from './interfaces/IService'; -// import { FEATURE_KEY } from './constants'; -import { OrganizationsService } from '@modules/organizations/service'; import { RequestContext } from '@modules/request-context/service'; import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants'; +import * as fs from 'fs'; @Injectable() export class DataSourcesService implements IDataSourcesService { @@ -32,8 +31,7 @@ export class DataSourcesService implements IDataSourcesService { protected readonly dataSourcesUtilService: DataSourcesUtilService, protected readonly abilityService: AbilityService, protected readonly appEnvironmentsUtilService: AppEnvironmentUtilService, - protected readonly pluginsServiceSelector: PluginsServiceSelector, - protected readonly organizationsService: OrganizationsService + protected readonly pluginsServiceSelector: PluginsServiceSelector ) {} async getForApp(query: GetQueryVariables, user: User): Promise<{ data_sources: object[] }> { @@ -120,7 +118,6 @@ export class DataSourcesService implements IDataSourcesService { if (kind === 'grpc') { const rootDir = process.cwd().split('/').slice(0, -1).join('/'); const protoFilePath = `${rootDir}/protos/service.proto`; - const fs = require('fs'); const filecontent = fs.readFileSync(protoFilePath, 'utf8'); const rcps = await this.dataSourcesUtilService.getServiceAndRpcNames(filecontent); diff --git a/server/src/modules/email/service.ts b/server/src/modules/email/service.ts index f2c9ceeb5d..ce1f60a5e4 100644 --- a/server/src/modules/email/service.ts +++ b/server/src/modules/email/service.ts @@ -56,6 +56,7 @@ export class EmailService implements IEmailService { this.WHITE_LABEL_TEXT = whiteLabelSettings?.white_label_text; this.WHITE_LABEL_LOGO = whiteLabelSettings?.white_label_logo; this.defaultWhiteLabelState = whiteLabelSettings?.default; + await this.emailUtilService.init(); } protected compileTemplate(templatePath: string, templateData: object) { @@ -77,7 +78,6 @@ export class EmailService implements IEmailService { redirectTo, } = payload; await this.init(organizationId); - await this.emailUtilService.init(organizationId); const isOrgInvite = organizationInvitationToken && sender && organizationName; const inviteUrl = generateInviteURL(invitationtoken, organizationInvitationToken, organizationId, null, redirectTo); const subject = isOrgInvite ? `Welcome to ${organizationName || 'ToolJet'}` : 'Set up your account!'; diff --git a/server/src/modules/git-sync/ability/guard.ts b/server/src/modules/git-sync/ability/guard.ts new file mode 100644 index 0000000000..86c09b8014 --- /dev/null +++ b/server/src/modules/git-sync/ability/guard.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { FeatureAbilityFactory } from '.'; +import { AbilityGuard } from '@modules/app/guards/ability.guard'; +import { ResourceDetails } from '@modules/app/types'; +import { MODULES } from '@modules/app/constants/modules'; +import { OrganizationGitSync } from '@entities/organization_git_sync.entity'; + +@Injectable() +export class FeatureAbilityGuard extends AbilityGuard { + protected getResource(): ResourceDetails { + return { + resourceType: MODULES.GIT_SYNC, + }; + } + + protected getAbilityFactory() { + return FeatureAbilityFactory; + } + + protected getSubjectType() { + return OrganizationGitSync; + } +} diff --git a/server/src/modules/git-sync/ability/index.ts b/server/src/modules/git-sync/ability/index.ts new file mode 100644 index 0000000000..c669c1cedb --- /dev/null +++ b/server/src/modules/git-sync/ability/index.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { Ability, AbilityBuilder, InferSubjects } from '@casl/ability'; +import { AbilityFactory } from '@modules/app/ability-factory'; +import { UserAllPermissions } from '@modules/app/types'; +import { FEATURE_KEY } from '../constants'; +import { OrganizationGitSync } from '@entities/organization_git_sync.entity'; + +type Subjects = InferSubjects | 'all'; +export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>; + +@Injectable() +export class FeatureAbilityFactory extends AbilityFactory { + protected getSubjectType() { + return OrganizationGitSync; + } + + protected defineAbilityFor( + can: AbilityBuilder['can'], + UserAllPermissions: UserAllPermissions, + extractedMetadata: { moduleName: string; features: string[] }, + request?: any + ): void { + const { superAdmin, isAdmin } = UserAllPermissions; + if (isAdmin || superAdmin) { + // Admin or Super Admin gets full access to all features + can(FEATURE_KEY.GET_ORGANIZATION_GIT, OrganizationGitSync); + can(FEATURE_KEY.GET_ORGANIZATION_GIT_STATUS, OrganizationGitSync); + can(FEATURE_KEY.CREATE_ORGANIZATION_GIT, OrganizationGitSync); + can(FEATURE_KEY.SAVE_PROVIDER_CONFIGS, OrganizationGitSync); + can(FEATURE_KEY.FINALIZE_CONFIGS, OrganizationGitSync); + can(FEATURE_KEY.UPDATE_PROVIDER_CONFIGS, OrganizationGitSync); + can(FEATURE_KEY.UPDATE_ORGANIZATION_GIT_STATUS, OrganizationGitSync); + can(FEATURE_KEY.DELETE_ORGANIZATION_GIT_CONFIGS, OrganizationGitSync); + return; + } + can(FEATURE_KEY.GET_ORGANIZATION_GIT_STATUS, OrganizationGitSync); + } +} diff --git a/server/src/modules/git-sync/constants/feature.ts b/server/src/modules/git-sync/constants/feature.ts new file mode 100644 index 0000000000..ee87e73c0d --- /dev/null +++ b/server/src/modules/git-sync/constants/feature.ts @@ -0,0 +1,17 @@ +import { FEATURE_KEY } from '.'; +import { MODULES } from '@modules/app/constants/modules'; +import { FeaturesConfig } from '../types'; +import { LICENSE_FIELD } from '@modules/licensing/constants'; + +export const FEATURES: FeaturesConfig = { + [MODULES.GIT_SYNC]: { + [FEATURE_KEY.GET_ORGANIZATION_GIT]: {}, + [FEATURE_KEY.GET_ORGANIZATION_GIT_STATUS]: {}, + [FEATURE_KEY.CREATE_ORGANIZATION_GIT]: { license: LICENSE_FIELD.GIT_SYNC }, + [FEATURE_KEY.SAVE_PROVIDER_CONFIGS]: { license: LICENSE_FIELD.GIT_SYNC }, + [FEATURE_KEY.FINALIZE_CONFIGS]: { license: LICENSE_FIELD.GIT_SYNC }, + [FEATURE_KEY.UPDATE_PROVIDER_CONFIGS]: { license: LICENSE_FIELD.GIT_SYNC }, + [FEATURE_KEY.UPDATE_ORGANIZATION_GIT_STATUS]: { license: LICENSE_FIELD.GIT_SYNC }, + [FEATURE_KEY.DELETE_ORGANIZATION_GIT_CONFIGS]: { license: LICENSE_FIELD.GIT_SYNC }, + }, +}; diff --git a/server/src/modules/git-sync/constants/index.ts b/server/src/modules/git-sync/constants/index.ts new file mode 100644 index 0000000000..42e87e647a --- /dev/null +++ b/server/src/modules/git-sync/constants/index.ts @@ -0,0 +1,10 @@ +export enum FEATURE_KEY { + GET_ORGANIZATION_GIT = 'GET_ORGANIZATION_GIT', + GET_ORGANIZATION_GIT_STATUS = 'GET_ORGANIZATION_GIT_STATUS', + CREATE_ORGANIZATION_GIT = 'CREATE_ORGANIZATION_GIT', + SAVE_PROVIDER_CONFIGS = 'SAVE_PROVIDER_CONFIGS', + FINALIZE_CONFIGS = 'FINALIZE_CONFIGS', + UPDATE_PROVIDER_CONFIGS = 'UPDATE_PROVIDER_CONFIGS', + UPDATE_ORGANIZATION_GIT_STATUS = 'UPDATE_ORGANIZATION_GIT_STATUS', + DELETE_ORGANIZATION_GIT_CONFIGS = 'DELETE_ORGANIZATION_GIT_CONFIGS', +} diff --git a/server/src/modules/git-sync/controller.ts b/server/src/modules/git-sync/controller.ts index f4087ffbe9..d3bf45ed47 100644 --- a/server/src/modules/git-sync/controller.ts +++ b/server/src/modules/git-sync/controller.ts @@ -8,8 +8,11 @@ import { import { User as UserEntity } from 'src/entities/user.entity'; import { IGitSyncController } from './Interfaces/IController'; import { ProviderConfigDTO } from './dto/provider-config.dto'; +import { InitModule } from '@modules/app/decorators/init-module'; +import { MODULES } from '@modules/app/constants/modules'; @Controller('git-sync') +@InitModule(MODULES.GIT_SYNC) export class GitSyncController implements IGitSyncController { constructor() {} diff --git a/server/src/modules/git-sync/constants/git_default.constant.ts b/server/src/modules/git-sync/error-constants/git_default.constant.ts similarity index 100% rename from server/src/modules/git-sync/constants/git_default.constant.ts rename to server/src/modules/git-sync/error-constants/git_default.constant.ts diff --git a/server/src/modules/git-sync/constants/gitsync_error.constant.ts b/server/src/modules/git-sync/error-constants/gitsync_error.constant.ts similarity index 100% rename from server/src/modules/git-sync/constants/gitsync_error.constant.ts rename to server/src/modules/git-sync/error-constants/gitsync_error.constant.ts diff --git a/server/src/modules/git-sync/module.ts b/server/src/modules/git-sync/module.ts index 61a55bc1a6..a1039cca52 100644 --- a/server/src/modules/git-sync/module.ts +++ b/server/src/modules/git-sync/module.ts @@ -7,6 +7,7 @@ import { OrganizationGitSyncRepository } from './repository'; import { VersionRepository } from '@modules/versions/repository'; import { AppGitRepository } from '@modules/app-git/repository'; import { SubModule } from '@modules/app/sub-module'; +import { FeatureAbilityFactory } from './ability'; export class GitSyncModule extends SubModule { static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { @@ -59,8 +60,15 @@ export class GitSyncModule extends SubModule { SSHGitSyncUtilityService, GitLabGitSyncUtilityService, SourceControlProviderService, + FeatureAbilityFactory, + ], + exports: [ + HTTPSGitSyncUtilityService, + SSHGitSyncUtilityService, + GitLabGitSyncUtilityService, + BaseGitSyncService, + BaseGitUtilService, ], - exports: [HTTPSGitSyncUtilityService, SSHGitSyncUtilityService, GitLabGitSyncUtilityService, BaseGitSyncService, BaseGitUtilService], }; } } diff --git a/server/src/modules/git-sync/types/index.ts b/server/src/modules/git-sync/types/index.ts new file mode 100644 index 0000000000..5d350d8d63 --- /dev/null +++ b/server/src/modules/git-sync/types/index.ts @@ -0,0 +1,18 @@ +import { FEATURE_KEY } from '../constants'; +import { FeatureConfig } from '@modules/app/types'; +import { MODULES } from '@modules/app/constants/modules'; + +interface Features { + [FEATURE_KEY.GET_ORGANIZATION_GIT]: FeatureConfig; + [FEATURE_KEY.GET_ORGANIZATION_GIT_STATUS]: FeatureConfig; + [FEATURE_KEY.CREATE_ORGANIZATION_GIT]: FeatureConfig; + [FEATURE_KEY.SAVE_PROVIDER_CONFIGS]: FeatureConfig; + [FEATURE_KEY.FINALIZE_CONFIGS]: FeatureConfig; + [FEATURE_KEY.UPDATE_PROVIDER_CONFIGS]: FeatureConfig; + [FEATURE_KEY.UPDATE_ORGANIZATION_GIT_STATUS]: FeatureConfig; + [FEATURE_KEY.DELETE_ORGANIZATION_GIT_CONFIGS]: FeatureConfig; +} + +export interface FeaturesConfig { + [MODULES.GIT_SYNC]: Features; +} diff --git a/server/src/modules/ignored/comment_users/comment_users.module.ts b/server/src/modules/ignored/comment_users/comment_users.module.ts deleted file mode 100644 index 7f5ca8fda9..0000000000 --- a/server/src/modules/ignored/comment_users/comment_users.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { CommentUsersController } from '@controllers/comment_users.controller'; -import { CommentUsersService } from '@services/comment_users.service'; -import { CaslModule } from '../casl/casl.module'; -import { CommentUsers } from 'src/entities/comment_user.entity'; -import { User } from 'src/entities/user.entity'; -import { AppVersion } from 'src/entities/app_version.entity'; - -@Module({ - controllers: [CommentUsersController], - imports: [TypeOrmModule.forFeature([CommentUsers, User, AppVersion]), CaslModule], - providers: [CommentUsersService], -}) -export class CommentUsersModule {} diff --git a/server/src/modules/ignored/comments/comment.module.ts b/server/src/modules/ignored/comments/comment.module.ts deleted file mode 100644 index 69b8e2b710..0000000000 --- a/server/src/modules/ignored/comments/comment.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { CommentController } from '@controllers/comment.controller'; -import { CommentService } from '@services/comment.service'; -import { CommentRepository } from '../../repositories/comment.repository'; -import { CaslModule } from '../casl/casl.module'; -import { User } from 'src/entities/user.entity'; -import { Organization } from 'src/entities/organization.entity'; -import { AppVersion } from 'src/entities/app_version.entity'; -import { CommentUsers } from 'src/entities/comment_user.entity'; -import { Comment } from 'src/entities/comment.entity'; -import { EmailModule } from '@modules/email/module'; - -@Module({ - controllers: [CommentController], - imports: [TypeOrmModule.forFeature([CommentUsers, AppVersion, User, Organization, Comment]), CaslModule, EmailModule], - providers: [CommentService, CommentRepository], -}) -export class CommentModule {} diff --git a/server/src/modules/ignored/import_export_resources/import_export_resources.module.ts b/server/src/modules/ignored/import_export_resources/import_export_resources.module.ts deleted file mode 100644 index 91d898eeb7..0000000000 --- a/server/src/modules/ignored/import_export_resources/import_export_resources.module.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { ImportExportResourcesController } from '@controllers/import_export_resources.controller'; -import { TooljetDbService } from '@services/tooljet_db.service'; -import { ImportExportResourcesService } from '@services/import_export_resources.service'; -import { AppImportExportService } from '@services/app_import_export.service'; -import { TooljetDbImportExportService } from '@services/tooljet_db_import_export_service'; -import { DataSourcesService } from '@services/data_sources.service'; -import { AppEnvironmentService } from '@ee/app-environments/service'; -import { Plugin } from 'src/entities/plugin.entity'; -import { PluginsHelper } from 'src/helpers/plugins.helper'; -import { CredentialsService } from '@services/credentials.service'; -import { DataSource } from 'src/entities/data_source.entity'; -import { PluginsModule } from '../plugins/plugins.module'; -import { Credential } from '../../../src/entities/credential.entity'; -import { CaslModule } from '../casl/casl.module'; -import { AppsService } from '@services/apps.service'; -import { App } from 'src/entities/app.entity'; -import { AppVersion } from 'src/entities/app_version.entity'; -import { AppUser } from 'src/entities/app_user.entity'; -import { User } from 'src/entities/user.entity'; -import { Organization } from 'src/entities/organization.entity'; -import { TooljetDbOperationsService } from '@services/tooljet_db_operations.service'; -import { PostgrestProxyService } from '@services/postgrest_proxy.service'; -import { TooljetDbModule } from '../tooljet_db/tooljet_db.module'; -import { UserResourcePermissionsModule } from '@modules/user_resource_permissions/user_resource_permissions.module'; -import { UsersModule } from '@modules/users/users.module'; -import { EncryptionModule } from '@modules/encryption/module'; - -const imports = [ - PluginsModule, - CaslModule, - TypeOrmModule.forFeature([User, Organization, AppUser, AppVersion, App, Credential, Plugin, DataSource]), - TooljetDbModule, - UserResourcePermissionsModule, - UsersModule, - EncryptionModule, -]; - -@Module({ - imports, - controllers: [ImportExportResourcesController], - providers: [ - ImportExportResourcesService, - AppImportExportService, - TooljetDbImportExportService, - DataSourcesService, - AppEnvironmentService, - TooljetDbService, - PluginsHelper, - AppsService, - CredentialsService, - TooljetDbOperationsService, - PostgrestProxyService, - ], - exports: [ImportExportResourcesService], -}) -export class ImportExportResourcesModule {} diff --git a/server/src/modules/ignored/instance_login_configs/instance_login_configs.module.ts b/server/src/modules/ignored/instance_login_configs/instance_login_configs.module.ts deleted file mode 100644 index d7fc1583ec..0000000000 --- a/server/src/modules/ignored/instance_login_configs/instance_login_configs.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { InstanceLoginConfigsController } from '@controllers/instance_login-configs.controller'; -import { InstanceSettingsModule } from '@instance-settings/module'; -import { OrganizationsModule } from '../organizations/organizations.module'; -import { SSOGuard } from '@modules/licensing/guards/sso/sso.guard'; -import { LDAPGuard } from '@modules/licensing/guards/sso/ldap.guard'; -import { OIDCGuard } from '@modules/licensing/guards/sso/oidc.guard'; -import { SAMLGuard } from '@modules/licensing/guards/sso/saml.guard'; -import { InstanceLoginConfigsService } from '@services/instance_login-configs.service'; -@Module({ - controllers: [InstanceLoginConfigsController], - providers: [InstanceLoginConfigsService, SSOGuard, OIDCGuard, LDAPGuard, SAMLGuard], - imports: [InstanceSettingsModule, OrganizationsModule], -}) -export class InstanceLoginConfigsModule {} diff --git a/server/src/modules/ignored/library_app/library_app.module.ts b/server/src/modules/ignored/library_app/library_app.module.ts deleted file mode 100644 index 6376d42137..0000000000 --- a/server/src/modules/ignored/library_app/library_app.module.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Module } from '@nestjs/common'; -import { LibraryAppsController } from '@controllers/library_apps.controller'; -import { LibraryAppCreationService } from '@services/library_app_creation.service'; -import { AppImportExportService } from '@services/app_import_export.service'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { App } from 'src/entities/app.entity'; -import { DataSourcesService } from '@services/data_sources.service'; -import { CredentialsService } from '@services/credentials.service'; -import { Credential } from 'src/entities/credential.entity'; -import { DataSource } from 'src/entities/data_source.entity'; -import { CaslModule } from '../casl/casl.module'; -import { FilesService } from '@services/files.service'; -import { File } from 'src/entities/file.entity'; -import { PluginsService } from '@services/plugins.service'; -import { Plugin } from 'src/entities/plugin.entity'; -import { PluginsHelper } from 'src/helpers/plugins.helper'; -import { AppEnvironmentService } from '@ee/app-environments/service'; -import { AppEnvironment } from 'src/entities/app_environments.entity'; -import { AppVersion } from 'src/entities/app_version.entity'; -import { User } from 'src/entities/user.entity'; -import { Organization } from 'src/entities/organization.entity'; -import { ImportExportResourcesModule } from '../import_export_resources/import_export_resources.module'; -import { TooljetDbOperationsService } from '@services/tooljet_db_operations.service'; -import { PostgrestProxyService } from '@services/postgrest_proxy.service'; -import { TooljetDbService } from '@services/tooljet_db.service'; -import { AppsService } from '@services/apps.service'; -import { AppUser } from 'src/entities/app_user.entity'; -import { TooljetDbModule } from '../tooljet_db/tooljet_db.module'; -import { UserResourcePermissionsModule } from '@modules/user_resource_permissions/user_resource_permissions.module'; -import { UsersModule } from '@modules/users/users.module'; -import { EncryptionModule } from '@modules/encryption/module'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([ - App, - Credential, - File, - Plugin, - DataSource, - AppEnvironment, - AppVersion, - User, - AppUser, - Organization, - ]), - CaslModule, - ImportExportResourcesModule, - TooljetDbModule, - UserResourcePermissionsModule, - UsersModule, - EncryptionModule, - ], - providers: [ - CredentialsService, - DataSourcesService, - LibraryAppCreationService, - AppImportExportService, - FilesService, - PluginsService, - PluginsHelper, - AppEnvironmentService, - TooljetDbOperationsService, - TooljetDbService, - PostgrestProxyService, - AppsService, - ], - controllers: [LibraryAppsController], -}) -export class LibraryAppModule {} diff --git a/server/src/modules/ignored/org_environment_variables/org_environment_variables.module.ts b/server/src/modules/ignored/org_environment_variables/org_environment_variables.module.ts deleted file mode 100644 index 034ac597af..0000000000 --- a/server/src/modules/ignored/org_environment_variables/org_environment_variables.module.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { OrgEnvironmentVariable } from '../../entities/org_envirnoment_variable.entity'; -import { OrgEnvironmentVariablesController } from '../../controllers/org_environment_variables.controller'; -import { OrgEnvironmentVariablesService } from '../../services/org_environment_variables.service'; -import { App } from 'src/entities/app.entity'; -import { User } from 'src/entities/user.entity'; -import { OrganizationUser } from 'src/entities/organization_user.entity'; -import { Organization } from 'src/entities/organization.entity'; -import { CaslModule } from '../casl/casl.module'; -import { FilesService } from '@services/files.service'; -import { Plugin } from 'src/entities/plugin.entity'; -import { PluginsService } from '@services/plugins.service'; -import { File } from 'src/entities/file.entity'; -import { AppsService } from '@services/apps.service'; -import { AppUser } from 'src/entities/app_user.entity'; -import { DataSource } from 'src/entities/data_source.entity'; -import { DataQuery } from 'src/entities/data_query.entity'; -import { FolderApp } from 'src/entities/folder_app.entity'; -import { AppVersion } from 'src/entities/app_version.entity'; -import { AppImportExportService } from '@services/app_import_export.service'; -import { DataSourcesService } from '@services/data_sources.service'; -import { CredentialsService } from '@services/credentials.service'; -import { Credential } from 'src/entities/credential.entity'; -import { AppEnvironment } from 'src/entities/app_environments.entity'; -import { PluginsHelper } from 'src/helpers/plugins.helper'; -import { AppEnvironmentService } from '@ee/app-environments/service'; -import { TooljetDbOperationsService } from '@services/tooljet_db_operations.service'; -import { TooljetDbService } from '@services/tooljet_db.service'; -import { PostgrestProxyService } from '@services/postgrest_proxy.service'; -import { TooljetDbModule } from '../tooljet_db/tooljet_db.module'; -import { UsersModule } from '@modules/users/users.module'; -import { EncryptionModule } from '@modules/encryption/module'; - -@Module({ - controllers: [OrgEnvironmentVariablesController], - imports: [ - TypeOrmModule.forFeature([ - App, - OrgEnvironmentVariable, - User, - OrganizationUser, - Organization, - File, - Plugin, - AppVersion, - AppUser, - DataSource, - DataQuery, - FolderApp, - Credential, - AppEnvironment, - ]), - CaslModule, - TooljetDbModule, - UsersModule, - EncryptionModule, - ], - providers: [ - OrgEnvironmentVariablesService, - AppsService, - FilesService, - PluginsService, - AppImportExportService, - DataSourcesService, - CredentialsService, - PluginsHelper, - AppEnvironmentService, - PostgrestProxyService, - TooljetDbOperationsService, - TooljetDbService, - ], -}) -export class OrgEnvironmentVariablesModule {} diff --git a/server/src/modules/ignored/repositories/comment.repository.ts b/server/src/modules/ignored/repositories/comment.repository.ts deleted file mode 100644 index eed717e203..0000000000 --- a/server/src/modules/ignored/repositories/comment.repository.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Comment } from '../entities/comment.entity'; -import { CreateCommentDto, UpdateCommentDto } from '../dto/comment.dto'; - -@Injectable() -export class CommentRepository extends Repository { - constructor( - @InjectRepository(Comment) - private commentRepository: Repository - ) { - super(commentRepository.target, commentRepository.manager, commentRepository.queryRunner); - } - - public async createComment( - createCommentDto: CreateCommentDto, - userId: string, - organizationId: string - ): Promise { - const { comment, threadId, appVersionsId } = createCommentDto; - const _comment = this.commentRepository.create({ - comment, - threadId, - userId, - organizationId, - appVersionsId, - }); - return this.commentRepository.save(_comment); - } - - public async editComment(updateCommentDto: UpdateCommentDto, editedComment: Comment): Promise { - const { comment, threadId } = updateCommentDto; - editedComment.comment = comment; - editedComment.threadId = threadId; - return this.commentRepository.save(editedComment); - } -} diff --git a/server/src/modules/ignored/repositories/group_permission.repository.ts b/server/src/modules/ignored/repositories/group_permission.repository.ts deleted file mode 100644 index d025d2df7c..0000000000 --- a/server/src/modules/ignored/repositories/group_permission.repository.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DataSource, EntityManager, Repository } from 'typeorm'; -import { GroupPermissions } from '@entities/group_permissions.entity'; - -interface IGroupPermissionRepository extends Repository { - getAllUserGroups(userId: string, organizationId: string, manager: EntityManager): Promise; -} - -export class GroupPermissionRepository extends Repository implements IGroupPermissionRepository { - constructor(private dataSource: DataSource) { - super(GroupPermissions, dataSource.createEntityManager()); - } - - getAllUserGroups(userId: string, organizationId: string, manager: EntityManager): Promise { - return manager.find(GroupPermissions, { - where: { - organizationId: organizationId, - groupUsers: { - userId: userId, - }, - }, - relations: ['groupUsers'], - }); - } -} diff --git a/server/src/modules/ignored/repositories/ssoConfigs.repository.ts b/server/src/modules/ignored/repositories/ssoConfigs.repository.ts deleted file mode 100644 index 284a174eaa..0000000000 --- a/server/src/modules/ignored/repositories/ssoConfigs.repository.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Repository } from 'typeorm'; -import { SSOConfigs, SSOType } from '@entities/sso_config.entity'; - -@Injectable() -export class SSOConfigsRepository extends Repository { - async findByOrganizationId(organizationId: string): Promise { - return this.find({ where: { organizationId } }); - } - - async findInstanceConfigs(): Promise { - return this.find({ where: { organizationId: null } }); - } - - async createOrUpdateSSOConfig(configData: Partial): Promise { - const existingConfig = await this.findOne({ - where: { sso: configData.sso, organizationId: configData.organizationId, configScope: configData.configScope }, - }); - - if (existingConfig) { - return this.save({ ...existingConfig, ...configData }); - } - - return this.save(this.create(configData)); - } - - async updateConfig(id: string, updateData: Partial): Promise { - await this.update(id, updateData); - return this.findOne({ where: { id } }); - } - - async deleteConfig(id: string): Promise { - await this.delete(id); - } - - async getSSOConfigsForOrganization(organizationId: string, sso: SSOType | string): Promise { - return this.findOne({ - where: { - organizationId, - sso: sso as SSOType, - }, - relations: ['organization'], - }); - } -} diff --git a/server/src/modules/ignored/repositories/thread.repository.ts b/server/src/modules/ignored/repositories/thread.repository.ts deleted file mode 100644 index 7216990153..0000000000 --- a/server/src/modules/ignored/repositories/thread.repository.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { CreateThreadDto, UpdateThreadDto } from '@dto/thread.dto'; -import { Thread } from '@entities/thread.entity'; - -@Injectable() -export class ThreadRepository extends Repository { - constructor( - @InjectRepository(Thread) - private threadRepository: Repository - ) { - super(threadRepository.target, threadRepository.manager, threadRepository.queryRunner); - } - - public async createThread(createThreadDto: CreateThreadDto, userId: string, organizationId: string): Promise { - const { x, y, appId, appVersionsId, pageId } = createThreadDto; - - const thread = this.threadRepository.create({ - x, - y, - appId, - userId, - organizationId, - appVersionsId, - pageId, - }); - - return await this.threadRepository.save(thread); - } - - public async editThread(updateThreadDto: UpdateThreadDto, editedThread: Thread): Promise { - const { x, y, isResolved } = updateThreadDto; - - editedThread.x = x; - editedThread.y = y; - editedThread.isResolved = isResolved; - - return await this.threadRepository.save(editedThread); - } -} diff --git a/server/src/modules/ignored/seeds/seeds.module.ts b/server/src/modules/ignored/seeds/seeds.module.ts deleted file mode 100644 index eb7ca9dfd7..0000000000 --- a/server/src/modules/ignored/seeds/seeds.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SeedsService } from '../../services/seeds.service'; -import { TooljetDbService } from '@services/tooljet_db.service'; -import { UserResourcePermissionsModule } from '@modules/user_resource_permissions/user_resource_permissions.module'; - -@Module({ - imports: [UserResourcePermissionsModule], - providers: [SeedsService, TooljetDbService], - exports: [SeedsService, TooljetDbService], -}) -export class SeedsModule {} diff --git a/server/src/modules/ignored/thread/thread.module.ts b/server/src/modules/ignored/thread/thread.module.ts deleted file mode 100644 index afa355424c..0000000000 --- a/server/src/modules/ignored/thread/thread.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { CaslModule } from '../casl/casl.module'; - -import { ThreadController } from '../../controllers/thread.controller'; -import { ThreadService } from '../../services/thread.service'; -import { ThreadRepository } from '../../repositories/thread.repository'; -import { Thread } from 'src/entities/thread.entity'; - -@Module({ - imports: [TypeOrmModule.forFeature([Thread]), CaslModule], - controllers: [ThreadController], - providers: [ThreadService, ThreadRepository], -}) -export class ThreadModule {} diff --git a/server/src/modules/ignored/tooljet_db/tooljet-db.types.ts b/server/src/modules/ignored/tooljet_db/tooljet-db.types.ts deleted file mode 100644 index 73b72013aa..0000000000 --- a/server/src/modules/ignored/tooljet_db/tooljet-db.types.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { QueryFailedError } from 'typeorm'; -import { InternalTable } from 'src/entities/internal_table.entity'; -import { capitalize } from 'lodash'; - -export const TJDB = { - character_varying: 'character varying' as const, - integer: 'integer' as const, - bigint: 'bigint' as const, - serial: 'serial' as const, - double_precision: 'double precision' as const, - boolean: 'boolean' as const, - timestampz: 'timestamp with time zone' as const, - jsonb: 'jsonb' as const, -}; - -export type TooljetDatabaseDataTypes = (typeof TJDB)[keyof typeof TJDB]; - -export type TooljetDatabaseColumn = { - column_name: string; - data_type: TooljetDatabaseDataTypes; - column_default: string | null; - character_maximum_length: number | null; - numeric_precision: number | null; - constraints_type: { - is_not_null: boolean; - is_primary_key: boolean; - is_unique: boolean; - }; - keytype: string | null; -}; - -export type TooljetDatabaseForeignKey = { - column_names: string[]; - referenced_table_name: string; - referenced_column_names: string[]; - on_update: string; - on_delete: string; - constraint_name: string; - referenced_table_id: string; -}; - -export type TooljetDatabaseTable = { - id: string; - table_name: string; - schema: { - columns: TooljetDatabaseColumn[]; - foreign_keys: TooljetDatabaseForeignKey[]; - }; -}; - -enum PostgresErrorCode { - UniqueViolation = '23505', - CheckViolation = '23514', - NotNullViolation = '23502', - ForeignKeyViolation = '23503', - DuplicateColumn = '42701', - UndefinedTable = '42P01', - PermissionDenied = '42501', - UndefinedFunction = '42883', -} - -export type TooljetDbActions = - | 'add_column' - | 'create_foreign_key' - | 'create_table' - | 'delete_foreign_key' - | 'drop_column' - | 'drop_table' - | 'edit_column' - | 'edit_table' - | 'join_tables' - | 'update_foreign_key' - | 'view_table' - | 'view_tables' - | 'sql_execution' - | 'bulk_upload' - | 'proxy_postgrest'; - -type ErrorCodeMappingItem = Partial>; -type ErrorCodeMapping = { - [key in PostgresErrorCode]: ErrorCodeMappingItem; -}; - -const errorCodeMapping: Partial = { - [PostgresErrorCode.NotNullViolation]: { - edit_column: 'Cannot add NOT NULL constraint as this column contains null values', - proxy_postgrest: 'Not null constraint violated for {{table}}.{{column}}', - }, - [PostgresErrorCode.UniqueViolation]: { - edit_column: 'Cannot add UNIQUE constraint as this column contains duplicate values', - proxy_postgrest: 'Unique constraint violated as {{value}} already exists in {{table}}.{{column}}', - bulk_upload: 'Duplicate value violates unique constraint', - }, - [PostgresErrorCode.UndefinedTable]: { - default: 'Could not find the table {{table}}.', - sql_execution: `Could not find the table or schema`, - }, - [PostgresErrorCode.ForeignKeyViolation]: { - proxy_postgrest: 'Update or delete on {{table}}.{{column}} with {{value}} violates foreign key constraint', - sql_execution: 'Update or delete on {{table}}.{{column}} with {{value}} violates foreign key constraint', - bulk_upload: 'Insert or update violates foreign key constraint', - }, - [PostgresErrorCode.PermissionDenied]: { - default: 'Insufficient privilege', - }, - [PostgresErrorCode.UndefinedFunction]: { - // proxy_postgrest: '{{fxName}} - aggregate function requires serial, integer, float or big int column type', - // join_tables: '{{fxName}} - aggregate function requires serial, integer, float or big int column type', - }, -}; - -export class PostgrestError extends Error { - code: string; - details: string; - hint: string; - message: string; - - constructor(postgrestErrorResponse: { code: string; details: string; hint: string; message: string }) { - super(); - - const { code, details, hint, message } = postgrestErrorResponse; - this.code = code; - this.details = details; - this.hint = hint; - this.message = message; - } - - toString(): string { - return `PostgrestError [${this.code}]: ${this.message}`; - } -} - -export class TooljetDatabaseError extends QueryFailedError { - public readonly code: string; - public readonly context: { - origin: TooljetDbActions; - internalTables: (InternalTable | { id: string; tableName: string })[]; - }; - public readonly queryError: QueryFailedError; - - constructor( - message: string, - context: { origin: TooljetDbActions; internalTables: InternalTable[] | { id: string; tableName: string }[] }, - errorObj: QueryFailedError - ) { - super(message, errorObj.parameters, errorObj.driverError); - this.context = context; - this.code = errorObj.driverError['code']; - this.queryError = errorObj; - } - - toString(): string { - const errorMessage = - errorCodeMapping[this.code]?.[this.context.origin] || - errorCodeMapping[this.code]?.['default'] || - capitalize(this.message); - return this.replaceErrorPlaceholders(errorMessage); - } - - replaceErrorPlaceholders(errorMessage: string): string { - let modifiedErrorMessage = errorMessage; - const internalTableEntries = this.context.internalTables.map(({ id, tableName }) => [id, tableName]); - - // Templates strings replacement current works in expectation that - // there will only be one table involved - const replaceTemplateStrings = (errorMessage: string, replacements: Record): string => { - return Object.entries(replacements).reduce((message, [key, value]) => { - return message.replace(new RegExp(`{{${key}}}`, 'g'), value); - }, errorMessage); - }; - - const replaceTableUUIDs = (errorMessage: string, internalTableEntries: string[][]): string => { - return internalTableEntries.reduce((acc, [key, value]) => { - return acc.replace(new RegExp(key, 'g'), value); - }, errorMessage); - }; - - const maskWorkspaceSchemaNameInErrorMessage = (errorMessage): string => { - let output = errorMessage - .replace(/workspace_[\w-]+\./g, '') - .replace(/'workspace_[\w-]+'\./g, "'") - .replace(/workspace_[\w-]+'?/g, '') - .replace(/\s*workspace_[\w-]+\s*/g, '') - .replace(/\s{2,}/g, ' ') - .replace(/"\s*"/g, '') - .trim(); - - output = output.trim(); - return output; - }; - - // Handle custom errors that are thrown from PostgREST with - // specific parsers for the error code - if (this.queryError.driverError instanceof PostgrestError) { - const parsedTableInfo = this.postgrestDetailsParser(); - if (parsedTableInfo) { - modifiedErrorMessage = replaceTemplateStrings(modifiedErrorMessage, parsedTableInfo); - } - } - - // TODO: Need to handle errors wherein multiple tables are involved when need arises - // - // Based on the internalTables in context replace the template placeholders - // that are used in the error message - if (this.context.internalTables.length === 1) { - const replacements = { table: this.context.internalTables[0].tableName }; - modifiedErrorMessage = replaceTemplateStrings(modifiedErrorMessage, replacements); - } - - // Based on the internalTables in context replace table UUIDs in - // the error message - modifiedErrorMessage = replaceTableUUIDs(modifiedErrorMessage, internalTableEntries); - modifiedErrorMessage = maskWorkspaceSchemaNameInErrorMessage(modifiedErrorMessage); - return modifiedErrorMessage; - } - - postgrestDetailsParser(): Record | null { - const parsers = { - [PostgresErrorCode.NotNullViolation]: () => { - const errorMessage = this.queryError.driverError.message; - const regex = /null value in column "(.*?)" of relation "(.*?)" violates not-null constraint/; - const matches = regex.exec(errorMessage); - const table = this.context.internalTables[0].tableName; - return { table, column: matches[1], value: matches[2] }; - }, - [PostgresErrorCode.UniqueViolation]: () => { - const errorMessage = this.queryError.driverError['details']; - const regex = /Key \((.*?)\)=\((.*?)\) already exists\./; - const matches = regex.exec(errorMessage); - const table = this.context.internalTables[0].tableName; - return { table, column: matches[1], value: matches[2] }; - }, - [PostgresErrorCode.ForeignKeyViolation]: () => { - const errorMessage = this.queryError.driverError['details']; - const regex = /Key \((.*?)\)=\((.*?)\) (is still referenced from table|is not present in table) "(.*?)"\./; - const matches = regex.exec(errorMessage); - const table = this.context.internalTables[0].tableName; - return { table, column: matches[1], value: matches[2], referencedTables: [matches[2]] }; - }, - [PostgresErrorCode.UndefinedFunction]: () => { - const errorMessage = this.queryError.driverError.message; - const regex = /function (\w+)\(([\w\s]+)\) does not exist/; - const matches = regex.exec(errorMessage); - const table = this.context.internalTables[0].tableName; - if (Array.isArray(matches) && matches.length) return { table, fxName: matches[1] }; - return null; - }, - }; - return parsers[this.code]?.() || null; - } -} diff --git a/server/src/modules/ignored/tooljet_db/tooljet_db.module.ts b/server/src/modules/ignored/tooljet_db/tooljet_db.module.ts deleted file mode 100644 index 5018b7fbb9..0000000000 --- a/server/src/modules/ignored/tooljet_db/tooljet_db.module.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Module, OnModuleInit } from '@nestjs/common'; -import { InjectEntityManager, TypeOrmModule } from '@nestjs/typeorm'; -import { Credential } from '../../../src/entities/credential.entity'; -import { TooljetDbController } from '@controllers/tooljet_db.controller'; -import { CaslModule } from '../casl/casl.module'; -import { TooljetDbService } from '@services/tooljet_db.service'; -import { PostgrestProxyService } from '@services/postgrest_proxy.service'; -import { InternalTable } from 'src/entities/internal_table.entity'; -import { AppUser } from 'src/entities/app_user.entity'; -import { TableCountGuard } from '@modules/licensing/guards/table.guard'; -import { TooljetDbBulkUploadService } from '@services/tooljet_db_bulk_upload.service'; -import { TooljetDbOperationsService } from '@services/tooljet_db_operations.service'; -import { ConfigService } from '@nestjs/config'; -import { EntityManager } from 'typeorm'; -import { reconfigurePostgrest } from './utils/helper'; -import { Logger } from 'nestjs-pino'; - -@Module({ - imports: [TypeOrmModule.forFeature([Credential, InternalTable, AppUser]), CaslModule], - controllers: [TooljetDbController], - providers: [ - TooljetDbService, - TooljetDbBulkUploadService, - TooljetDbOperationsService, - PostgrestProxyService, - TableCountGuard, - ], - exports: [TooljetDbService, TooljetDbBulkUploadService, TooljetDbOperationsService, PostgrestProxyService], -}) -export class TooljetDbModule implements OnModuleInit { - constructor( - private logger: Logger, - private configService: ConfigService, - @InjectEntityManager('tooljetDb') - private readonly tooljetDbManager: EntityManager - ) {} - - async onModuleInit() { - if (!process.env.WORKER) { - const tooljtDbUser = this.configService.get('TOOLJET_DB_USER'); - const statementTimeout = this.configService.get('TOOLJET_DB_STATEMENT_TIMEOUT') || 60000; - const statementTimeoutInSecs = Number.isNaN(Number(statementTimeout)) ? 60 : Number(statementTimeout) / 1000; - - await reconfigurePostgrest(this.tooljetDbManager, { - user: tooljtDbUser, - enableAggregates: true, - statementTimeoutInSecs: statementTimeoutInSecs, - }); - await this.tooljetDbManager.query("NOTIFY pgrst, 'reload schema'"); - } - } -} diff --git a/server/src/modules/ignored/tooljet_db/utils/helper.ts b/server/src/modules/ignored/tooljet_db/utils/helper.ts deleted file mode 100644 index 5dab99a0d2..0000000000 --- a/server/src/modules/ignored/tooljet_db/utils/helper.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { EntityManager } from 'typeorm'; - -export async function reconfigurePostgrest( - tooljetDbManager: EntityManager, - options: { user: string; enableAggregates: boolean; statementTimeoutInSecs: number } -) { - try { - await tooljetDbManager.transaction(async (transactionalEntityManager) => { - await transactionalEntityManager.queryRunner.query('CREATE SCHEMA IF NOT EXISTS postgrest'); - await transactionalEntityManager.queryRunner.query(`GRANT USAGE ON SCHEMA postgrest to ${options.user}`); - await transactionalEntityManager.queryRunner.query(`create or replace function postgrest.pre_config() - returns void as $$ - select - set_config('pgrst.db_aggregates_enabled', '${options.enableAggregates}', false); - select - set_config('pgrst.db_schemas', string_agg(nspname, ','), true) - from pg_namespace - where nspname like 'workspace_%'; - $$ language sql; - `); - await transactionalEntityManager.queryRunner.query( - `ALTER ROLE ${options.user} SET statement_timeout TO '${options.statementTimeoutInSecs}s'` - ); - }); - } catch (error) { - console.error('The tooljet database reconfiguration process encountered an error.', error); - throw error; - } -} diff --git a/server/src/modules/ignored/webhooks/webhooks.module.ts b/server/src/modules/ignored/webhooks/webhooks.module.ts deleted file mode 100644 index d512792cca..0000000000 --- a/server/src/modules/ignored/webhooks/webhooks.module.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { CaslModule } from '../casl/casl.module'; - -import { App } from 'src/entities/app.entity'; -import { AppVersion } from 'src/entities/app_version.entity'; -import { Credential } from 'src/entities/credential.entity'; -import { DataQuery } from 'src/entities/data_query.entity'; -import { DataSource } from 'src/entities/data_source.entity'; -import { File } from 'src/entities/file.entity'; -import { OrgEnvironmentVariable } from 'src/entities/org_envirnoment_variable.entity'; -import { Organization } from 'src/entities/organization.entity'; -import { Plugin } from 'src/entities/plugin.entity'; -import { User } from 'src/entities/user.entity'; -import { WorkflowExecution } from 'src/entities/workflow_execution.entity'; -import { WorkflowExecutionEdge } from 'src/entities/workflow_execution_edge.entity'; -import { WorkflowExecutionNode } from 'src/entities/workflow_execution_node.entity'; - -import { AppEnvironmentService } from '@ee/app-environments/service'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { CredentialsService } from '@services/credentials.service'; -import { DataQueriesService } from '@modules/data-queries/service'; -import { DataSourcesService } from '@services/data_sources.service'; -import { FilesService } from '@services/files.service'; -import { PluginsHelper } from 'src/helpers/plugins.helper'; -import { ThrottlerModule } from '@nestjs/throttler'; -import { WorkflowExecutionsController } from '@controllers/workflow_executions_controller'; -import { WorkflowExecutionsService } from '@services/workflow_executions.service'; -import { WorkflowWebhooksController } from '@controllers/workflow_webhooks.controller'; -import { WorkflowWebhooksListener } from '../../listeners/workflow_webhooks.listener'; -import { WorkflowWebhooksService } from '@services/workflow_webhooks.service'; -import { TooljetDbOperationsService } from '@services/tooljet_db_operations.service'; -import { TooljetDbService } from '@services/tooljet_db.service'; -import { PostgrestProxyService } from '@services/postgrest_proxy.service'; -import { OrganizationConstantsService } from '@modules/organization-constants/service'; -import { OrganizationConstant } from 'src/entities/organization_constants.entity'; -import { UserResourcePermissionsModule } from '@modules/user_resource_permissions/user_resource_permissions.module'; -import { UsersModule } from '@modules/users/users.module'; -import { EncryptionModule } from '@modules/encryption/module'; - -@Module({ - imports: [ - UserResourcePermissionsModule, - ThrottlerModule.forRootAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (config: ConfigService) => [ - { - ttl: config.get('WEBHOOK_THROTTLE_TTL') || 60000, - limit: config.get('WEBHOOK_THROTTLE_LIMIT') || 100, - }, - ], - }), - TypeOrmModule.forFeature([ - App, - AppVersion, - Credential, - DataQuery, - DataSource, - File, - OrgEnvironmentVariable, - Organization, - Plugin, - User, - WorkflowExecution, - WorkflowExecutionEdge, - WorkflowExecutionNode, - OrganizationConstant, - ]), - CaslModule, - UsersModule, - EncryptionModule, - ], - providers: [ - AppEnvironmentService, - CredentialsService, - DataQueriesService, - DataSourcesService, - FilesService, - PluginsHelper, - WorkflowExecutionsService, - WorkflowWebhooksListener, - WorkflowWebhooksService, - TooljetDbOperationsService, - TooljetDbService, - PostgrestProxyService, - OrganizationConstantsService, - ], - controllers: [WorkflowExecutionsController, WorkflowWebhooksController], -}) -export class WebhooksModule {} diff --git a/server/src/modules/ignored/worker.module.ts b/server/src/modules/ignored/worker.module.ts deleted file mode 100644 index 39f914ee71..0000000000 --- a/server/src/modules/ignored/worker.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { WorkerService } from '@services/worker.service'; - -@Module({ - imports: [], - providers: [WorkerService], - controllers: [], - exports: [], -}) -export class WorkerModule {} diff --git a/server/src/modules/licensing/ability/index.ts b/server/src/modules/licensing/ability/index.ts index 0092823b5b..9502e4fd6a 100644 --- a/server/src/modules/licensing/ability/index.ts +++ b/server/src/modules/licensing/ability/index.ts @@ -33,6 +33,7 @@ export class FeatureAbilityFactory extends AbilityFactory FEATURE_KEY.GET_ORGANIZATION_LIMITS, FEATURE_KEY.GET_USER_LIMITS, FEATURE_KEY.GET_WORKFLOW_LIMITS, + FEATURE_KEY.GENERATE_CLOUD_TRIAL_LICENSE, ], InstanceSettings ); diff --git a/server/src/modules/licensing/configs/LicenseBase.ts b/server/src/modules/licensing/configs/LicenseBase.ts index 5c8d82025a..084cc05d83 100644 --- a/server/src/modules/licensing/configs/LicenseBase.ts +++ b/server/src/modules/licensing/configs/LicenseBase.ts @@ -1,4 +1,4 @@ -import { LICENSE_LIMIT, LICENSE_TYPE, PLAN_DETAILS } from '@modules/licensing/constants'; +import { LICENSE_LIMIT, LICENSE_TYPE } from '@modules/licensing/constants'; import { Terms } from '@modules/licensing/interfaces/terms'; import { BUSINESS_PLAN_TERMS, @@ -80,7 +80,11 @@ export default class LicenseBase { this._type = LICENSE_TYPE.BASIC; return; } - this._expiryDate = expiryDate ? new Date(expiryDate) : (licenseData?.expiry ? new Date(`${licenseData?.expiry} 23:59:59`) : null); + this._expiryDate = expiryDate + ? new Date(expiryDate) + : licenseData?.expiry + ? new Date(`${licenseData?.expiry} 23:59:59`) + : null; this._startDate = startDate; this._isFlexiblePlan = licenseData?.plan?.isFlexible === true; this._appsCount = licenseData?.apps; diff --git a/server/src/modules/licensing/constants/PlanTerms.ts b/server/src/modules/licensing/constants/PlanTerms.ts index cfbfb851bd..0c1791f7bc 100644 --- a/server/src/modules/licensing/constants/PlanTerms.ts +++ b/server/src/modules/licensing/constants/PlanTerms.ts @@ -86,6 +86,9 @@ export const CLOUD_EDITION_SETTINGS = { SMTP_ENV_CONFIGURED: { value: 'true', }, + ENABLE_SIGNUP: { + value: 'true', + }, }; export const BUSINESS_PLAN_TERMS = { diff --git a/server/src/modules/licensing/constants/features.ts b/server/src/modules/licensing/constants/features.ts index 3eb3894474..c98d276031 100644 --- a/server/src/modules/licensing/constants/features.ts +++ b/server/src/modules/licensing/constants/features.ts @@ -24,5 +24,9 @@ export const FEATURES: FeaturesConfig = { }, [FEATURE_KEY.GET_WORKFLOW_LIMITS]: {}, [FEATURE_KEY.GET_USER_LIMITS]: {}, + [FEATURE_KEY.GENERATE_CLOUD_TRIAL_LICENSE]: {}, + [FEATURE_KEY.GENERATE_EE_TRIAL_LICENSE]: { + isPublic: true, + }, }, }; diff --git a/server/src/modules/licensing/constants/index.ts b/server/src/modules/licensing/constants/index.ts index 2978eb9285..6c65f4cc73 100644 --- a/server/src/modules/licensing/constants/index.ts +++ b/server/src/modules/licensing/constants/index.ts @@ -168,6 +168,8 @@ export enum FEATURE_KEY { GET_ORGANIZATION_LIMITS = 'get_organization_limits', GET_USER_LIMITS = 'get_user_limits', GET_WORKFLOW_LIMITS = 'get_workflow_limits', + GENERATE_CLOUD_TRIAL_LICENSE = 'generate_cloud_trial_license', + GENERATE_EE_TRIAL_LICENSE = 'generate_ee_trial_license', } export const ORGANIZATION_INSTANCE_KEY = 'INSTANCE'; diff --git a/server/src/modules/licensing/guards/auditLog.guard.ts b/server/src/modules/licensing/guards/auditLog.guard.ts index 8ce49e453a..d312c64ed2 100644 --- a/server/src/modules/licensing/guards/auditLog.guard.ts +++ b/server/src/modules/licensing/guards/auditLog.guard.ts @@ -13,7 +13,7 @@ export class AuditLogsDurationGuard implements CanActivate { auditLogsEnabled, } = await this.licenseTermsService.getLicenseTerms( [LICENSE_FIELD.STATUS, LICENSE_FIELD.MAX_DURATION_FOR_AUDIT_LOGS, LICENSE_FIELD.AUDIT_LOGS], - request?.user?.organization?.id + request?.user?.organizationId ); if (!auditLogsEnabled) { throw new HttpException( diff --git a/server/src/modules/licensing/guards/editorUser.guard.ts b/server/src/modules/licensing/guards/editorUser.guard.ts index 4a7c232952..13a4fb1ae4 100644 --- a/server/src/modules/licensing/guards/editorUser.guard.ts +++ b/server/src/modules/licensing/guards/editorUser.guard.ts @@ -21,7 +21,7 @@ export class EditorUserCountGuard implements CanActivate { const request = context.switchToHttp().getRequest(); const organizationId = request.body.organizationId; const isWorkspaceSignup = !!organizationId; - if (!isWorkspaceSignup && getTooljetEdition() === TOOLJET_EDITIONS.Cloud) { + if (isWorkspaceSignup && getTooljetEdition() === TOOLJET_EDITIONS.Cloud) { // Not needed for cloud edition, as it is not used in the cloud return true; } diff --git a/server/src/modules/licensing/guards/user.guard.ts b/server/src/modules/licensing/guards/user.guard.ts index 0ae7b82039..3bee4ef0ad 100644 --- a/server/src/modules/licensing/guards/user.guard.ts +++ b/server/src/modules/licensing/guards/user.guard.ts @@ -20,7 +20,7 @@ export class UserCountGuard implements CanActivate { : await this.licenseTermsService.getLicenseTermsInstance(LICENSE_FIELD.TOTAL_USERS); if ( totalUsers !== LICENSE_LIMIT.UNLIMITED && - (await this.licenseCountsService.getUsersCount(organizationId)) >= totalUsers + (await this.licenseCountsService.getUsersCount(organizationId, true)) >= totalUsers ) { throw new HttpException('License violation - Maximum user limit reached', 451); } diff --git a/server/src/modules/licensing/service.ts b/server/src/modules/licensing/service.ts index eccde55de1..41abdd9a94 100644 --- a/server/src/modules/licensing/service.ts +++ b/server/src/modules/licensing/service.ts @@ -24,7 +24,7 @@ export class LicenseService implements ILicenseService { async plans(): Promise<{ plans: any }> { try { /* TODO API request to the cloud server to a specific version license plans */ - } catch (error) { + } catch { throw new HttpException('Failed to fetch plans', 500); } return { plans: PLAN_DETAILS }; diff --git a/server/src/modules/licensing/services/terms.service.ts b/server/src/modules/licensing/services/terms.service.ts index e691da567c..025b8d6fa5 100644 --- a/server/src/modules/licensing/services/terms.service.ts +++ b/server/src/modules/licensing/services/terms.service.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { LICENSE_FIELD } from '@modules/licensing/constants'; +import { LICENSE_FIELD, ORGANIZATION_INSTANCE_KEY } from '@modules/licensing/constants'; import { LicenseInitService } from '../interfaces/IService'; import { LicenseTermsService as ILicenseTermsService } from '../interfaces/IService'; import License from '../configs/License'; - @Injectable() export class LicenseTermsService extends ILicenseTermsService { constructor(protected readonly licenseInitService: LicenseInitService) { @@ -11,7 +10,7 @@ export class LicenseTermsService extends ILicenseTermsService { } async getLicenseTermsInstance(type: LICENSE_FIELD | LICENSE_FIELD[]): Promise { - throw new Error('Method not implemented. This method is not supported on cloud.'); + return this.getLicenseTerms(type, ORGANIZATION_INSTANCE_KEY); } // This function should be called to get a specific license term diff --git a/server/src/modules/licensing/types/index.ts b/server/src/modules/licensing/types/index.ts index 4c49b18dbe..a345e6e6c5 100644 --- a/server/src/modules/licensing/types/index.ts +++ b/server/src/modules/licensing/types/index.ts @@ -17,6 +17,8 @@ interface Features { [FEATURE_KEY.UPDATE_LICENSE]: FeatureConfig; [FEATURE_KEY.GET_ORGANIZATION_LIMITS]: FeatureConfig; [FEATURE_KEY.GET_APP_LIMITS]: FeatureConfig; + [FEATURE_KEY.GENERATE_CLOUD_TRIAL_LICENSE]: FeatureConfig; + [FEATURE_KEY.GENERATE_EE_TRIAL_LICENSE]: FeatureConfig; } export interface FeaturesConfig { diff --git a/server/src/modules/onboarding/service.ts b/server/src/modules/onboarding/service.ts index 9bad731b4a..69d1146fdc 100644 --- a/server/src/modules/onboarding/service.ts +++ b/server/src/modules/onboarding/service.ts @@ -40,7 +40,7 @@ import { USER_ROLE } from '@modules/group-permissions/constants'; import { ActivateAccountWithTokenDto } from '@modules/onboarding/dto/activate-account-with-token.dto'; import { AppSignupDto } from '@modules/auth/dto'; import { SIGNUP_ERRORS } from 'src/helpers/errors.constants'; -const uuid = require('uuid'); +import * as uuid from 'uuid'; import { INSTANCE_SYSTEM_SETTINGS, INSTANCE_USER_SETTINGS } from '@modules/instance-settings/constants'; import { ResendInviteDto } from '@modules/onboarding/dto/resend-invite.dto'; import { EventEmitter2 } from '@nestjs/event-emitter'; @@ -399,6 +399,7 @@ export class OnboardingService implements IOnboardingService { type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, payload: { to: user.email, + // eslint-disable-next-line no-constant-binary-expression name: `${user.firstName} ${user.lastName}` || '', invitationtoken: user.invitationToken, organizationInvitationToken: `${organizationUser.invitationToken}`, diff --git a/server/src/modules/onboarding/util.service.ts b/server/src/modules/onboarding/util.service.ts index 8c58979565..1d2dfb90a6 100644 --- a/server/src/modules/onboarding/util.service.ts +++ b/server/src/modules/onboarding/util.service.ts @@ -6,7 +6,7 @@ import { OrganizationUser } from '../../entities/organization_user.entity'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { TrialUserDto } from '@modules/onboarding/dto/user.dto'; import { LicenseCountsService } from '../licensing/services/count.service'; -import { LICENSE_TRIAL_API } from '../licensing/constants'; +import { LICENSE_TRIAL_API, ORGANIZATION_INSTANCE_KEY } from '../licensing/constants'; import got from 'got/dist/source'; import { HttpException } from '@nestjs/common'; import { fullName, generateNextNameAndSlug, generateOrgInviteURL } from 'src/helpers/utils.helper'; @@ -40,7 +40,7 @@ import { UserOnboardingDetails } from './types'; import { OnboardingStatus } from './constants'; import { IOnboardingUtilService } from './interfaces/IUtilService'; import { SetupOrganizationsUtilService } from '@modules/setup-organization/util.service'; -const uuid = require('uuid'); +import * as uuid from 'uuid'; @Injectable() export class OnboardingUtilService implements IOnboardingUtilService { @@ -72,7 +72,10 @@ export class OnboardingUtilService implements IOnboardingUtilService { const otherData = { companySize, role, phoneNumber }; await dbTransactionWrap(async (manager: EntityManager) => { - const { editor, viewer } = await this.licenseCountsService.fetchTotalViewerEditorCount('INSTANCE',manager); + const { editor, viewer } = await this.licenseCountsService.fetchTotalViewerEditorCount( + ORGANIZATION_INSTANCE_KEY, + manager + ); const body = { hostname, @@ -252,12 +255,17 @@ export class OnboardingUtilService implements IOnboardingUtilService { case hasWorkspaceInviteButUserWantsInstanceSignup: { const firstTimeSignup = ![SOURCE.SIGNUP, SOURCE.WORKSPACE_SIGNUP].includes(existingUser.source as SOURCE); if (firstTimeSignup) { - if(defaultWorkspace) { - return this.updateExistingUserDefaultWorkspace({ - password, - firstName, - lastName - },existingUser, defaultWorkspace, manager); + if (defaultWorkspace) { + return this.updateExistingUserDefaultWorkspace( + { + password, + firstName, + lastName, + }, + existingUser, + defaultWorkspace, + manager + ); } /* Invite user doing instance signup. So reset name fields and set password */ @@ -266,7 +274,7 @@ export class OnboardingUtilService implements IOnboardingUtilService { (await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) === 'true'; - if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) { + if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) { const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(existingUser.id); if (personalWorkspaces.length) { defaultOrganizationId = personalWorkspaces[0].organizationId; @@ -629,7 +637,7 @@ export class OnboardingUtilService implements IOnboardingUtilService { // Create user with end-user role in default workspace const lifeCycleParms = getUserStatusAndSource(lifecycleEvents.USER_SIGN_UP); - + const user = await this.create( { email, @@ -661,8 +669,8 @@ export class OnboardingUtilService implements IOnboardingUtilService { // Send welcome email this.eventEmitter.emit('emailEvent', { - type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, - payload: { + type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, + payload: { to: user.email, name: user.firstName, invitationtoken: user.invitationToken, @@ -686,10 +694,10 @@ export class OnboardingUtilService implements IOnboardingUtilService { where: { userId: existingUser.id, organizationId: defaultWorkspace.id, - } + }, }); - if(existingOrgUser){ + if (existingOrgUser) { throw new NotAcceptableException( 'The user is already registered. Please check your inbox for the activation link' ); @@ -708,20 +716,20 @@ export class OnboardingUtilService implements IOnboardingUtilService { manager ); - await this.organizationUserRepository.createOne( - existingUser, - defaultWorkspace, - true, - manager, - WORKSPACE_USER_SOURCE.SIGNUP - ); + await this.organizationUserRepository.createOne( + existingUser, + defaultWorkspace, + true, + manager, + WORKSPACE_USER_SOURCE.SIGNUP + ); - // Add end-user role in default workspace if not already present - await this.rolesUtilService.addUserRole( - defaultWorkspace.id, - { role: USER_ROLE.END_USER, userId: existingUser.id }, - manager - ); + // Add end-user role in default workspace if not already present + await this.rolesUtilService.addUserRole( + defaultWorkspace.id, + { role: USER_ROLE.END_USER, userId: existingUser.id }, + manager + ); // Validate license await this.licenseUserService.validateUser(manager, existingUser?.defaultOrganizationId); diff --git a/server/src/modules/organization-payments/ability/index.ts b/server/src/modules/organization-payments/ability/index.ts index 01c16af075..18ed7f0ad6 100644 --- a/server/src/modules/organization-payments/ability/index.ts +++ b/server/src/modules/organization-payments/ability/index.ts @@ -6,7 +6,7 @@ import { FEATURE_KEY } from '@modules/organization-payments/constants'; import { OrganizationSubscription } from '@entities/organization_subscription.entity'; type Subjects = InferSubjects | 'all'; -export type OrganizationConstantAbility = Ability<[FEATURE_KEY, Subjects]>; +export type OrganizationPaymentsAbility = Ability<[FEATURE_KEY, Subjects]>; @Injectable() export class FeatureAbilityFactory extends AbilityFactory { @@ -15,25 +15,26 @@ export class FeatureAbilityFactory extends AbilityFactory } protected defineAbilityFor( - can: AbilityBuilder['can'], + can: AbilityBuilder['can'], userPermissions: UserAllPermissions ): void { - const { superAdmin, isAdmin } = userPermissions; + const { isAdmin } = userPermissions; - if (superAdmin || isAdmin) { + if (isAdmin) { can( [ FEATURE_KEY.CREATE_PORTAL_LINK, - FEATURE_KEY.GENERATE_PAYMENT_LINK, + FEATURE_KEY.GET_CURRENT_PLAN_DETAILS, FEATURE_KEY.GET_PRORATION, FEATURE_KEY.GET_REDIRECT_URL, FEATURE_KEY.GET_UPCOMING_INVOICE, - FEATURE_KEY.STRIPE_WEBHOOK, FEATURE_KEY.UPDATE_INVOICE, FEATURE_KEY.UPDATE_SUBSCRIPTION, ], OrganizationSubscription ); } + + can([FEATURE_KEY.STRIPE_WEBHOOK], OrganizationSubscription); } } diff --git a/server/src/modules/organization-payments/constants/feature.ts b/server/src/modules/organization-payments/constants/feature.ts index ce230c8c62..1cce121230 100644 --- a/server/src/modules/organization-payments/constants/feature.ts +++ b/server/src/modules/organization-payments/constants/feature.ts @@ -5,11 +5,13 @@ import { FeaturesConfig } from '@modules/organization-payments/types'; export const FEATURES: FeaturesConfig = { [MODULES.ORGANIZATION_PAYMENTS]: { [FEATURE_KEY.CREATE_PORTAL_LINK]: {}, - [FEATURE_KEY.GENERATE_PAYMENT_LINK]: {}, + [FEATURE_KEY.GET_CURRENT_PLAN_DETAILS]: {}, [FEATURE_KEY.GET_PRORATION]: {}, [FEATURE_KEY.GET_REDIRECT_URL]: {}, [FEATURE_KEY.GET_UPCOMING_INVOICE]: {}, - [FEATURE_KEY.STRIPE_WEBHOOK]: {}, + [FEATURE_KEY.STRIPE_WEBHOOK]: { + isPublic: true, + }, [FEATURE_KEY.UPDATE_INVOICE]: {}, [FEATURE_KEY.UPDATE_SUBSCRIPTION]: {}, }, diff --git a/server/src/modules/organization-payments/constants/index.ts b/server/src/modules/organization-payments/constants/index.ts index a66d218c8a..073ead8dbe 100644 --- a/server/src/modules/organization-payments/constants/index.ts +++ b/server/src/modules/organization-payments/constants/index.ts @@ -1,6 +1,6 @@ export enum FEATURE_KEY { GET_UPCOMING_INVOICE = 'get_upcoming_invoice', - GENERATE_PAYMENT_LINK = 'generate_payment_link', + GET_CURRENT_PLAN_DETAILS = 'get_current_plan_details', UPDATE_INVOICE = 'update_invoice', GET_PRORATION = 'get_proration', CREATE_PORTAL_LINK = 'create_portal_link', diff --git a/server/src/modules/organization-payments/controller.ts b/server/src/modules/organization-payments/controller.ts index 058661db01..8648893bb5 100644 --- a/server/src/modules/organization-payments/controller.ts +++ b/server/src/modules/organization-payments/controller.ts @@ -1,9 +1,50 @@ import { InitModule } from '@modules/app/decorators/init-module'; import { MODULES } from '@modules/app/constants/modules'; -import { Controller } from '@nestjs/common'; +import { Controller, UseGuards } from '@nestjs/common'; +import { IOrganizationPaymentController } from './interfaces/IController'; +import { ProrationDto, PortalDto, PaymentRedirectDto } from './dto'; +import { JwtAuthGuard } from '@modules/session/guards/jwt-auth.guard'; +import { FeatureAbilityGuard } from './ability/guard'; @Controller('organization/payment') @InitModule(MODULES.ORGANIZATION_PAYMENTS) -export class OrganizationPaymentController { +@UseGuards(JwtAuthGuard, FeatureAbilityGuard) +export class OrganizationPaymentController implements IOrganizationPaymentController { constructor() {} + getUpcomingInvoice(organizationId: string): Promise { + throw new Error('Method not implemented.'); + } + getCurrentPlan(organizationId: string): Promise { + throw new Error('Method not implemented.'); + } + updateInvoice(organizationId: string, id: string): Promise { + throw new Error('Method not implemented.'); + } + getProration(organizationId: string, prorationDto: ProrationDto): Promise { + throw new Error('Method not implemented.'); + } + getPortalLink(portalDto: PortalDto): Promise { + throw new Error('Method not implemented.'); + } + updateSubscription(organizationId: string, prorationDto: ProrationDto): Promise { + throw new Error('Method not implemented.'); + } + webhookCheckoutSessionCompleteHandler(paymentObject: any): Promise { + throw new Error('Method not implemented.'); + } + upcomingInvoiceHandler(invoiceObject: any): Promise { + throw new Error('Method not implemented.'); + } + paymentFailedHandler(invoiceObject: any): Promise { + throw new Error('Method not implemented.'); + } + subscriptionUpdateHandler(subscription: any): Promise { + throw new Error('Method not implemented.'); + } + webhookInvoicePaidHandler(invoiceObject: any): Promise { + throw new Error('Method not implemented.'); + } + getRedirectUrl(user: any, paymentRedirectDto: PaymentRedirectDto): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/server/src/modules/organization-payments/interfaces/IController.ts b/server/src/modules/organization-payments/interfaces/IController.ts new file mode 100644 index 0000000000..4fdd86d3f8 --- /dev/null +++ b/server/src/modules/organization-payments/interfaces/IController.ts @@ -0,0 +1,27 @@ +import { PortalDto, ProrationDto, PaymentRedirectDto } from '@modules/organization-payments/dto'; + +export interface IOrganizationPaymentController { + getUpcomingInvoice(organizationId: string): Promise; + + getCurrentPlan(organizationId: string): Promise; + + updateInvoice(organizationId, string, id: string): Promise; + + getProration(organizationId: string, prorationDto: ProrationDto): Promise; + + getPortalLink(portalDto: PortalDto): Promise; + + updateSubscription(organizationId: string, prorationDto: ProrationDto): Promise; + + webhookCheckoutSessionCompleteHandler(paymentObject: any): Promise; + + upcomingInvoiceHandler(invoiceObject: any): Promise; + + paymentFailedHandler(invoiceObject: any): Promise; + + subscriptionUpdateHandler(subscription: any): Promise; + + webhookInvoicePaidHandler(invoiceObject: any): Promise; + + getRedirectUrl(user: any, paymentRedirectDto: PaymentRedirectDto): Promise; +} diff --git a/server/src/modules/organization-payments/interfaces/IService.ts b/server/src/modules/organization-payments/interfaces/IService.ts new file mode 100644 index 0000000000..5bc14514db --- /dev/null +++ b/server/src/modules/organization-payments/interfaces/IService.ts @@ -0,0 +1,47 @@ +import { OrganizationSubscriptionInvoice } from '@entities/organization_subscription_invoice.entity'; +import { PaymentRedirectDto, PortalDto, ProrationDto } from '../dto'; +import { OrganizationSubscription } from '@entities/organization_subscription.entity'; + +export interface IOrganizationPaymentService { + getUpcomingInvoice(organizationId: string): Promise; + + getCurrentPlan(organizationId: string): Promise; + + updateInvoice(updateCondition?: any, updateObject?: any): Promise; + + getProration(organizationId: string, prorationDto: ProrationDto): Promise; + + getPortalLink(portalDto: PortalDto): Promise; + + updateSubscription(organizationId: string, updatedSubscription: ProrationDto): Promise; + + licenseUpgradeValidation(organizationId: string, checkParam: any, manager?: any): Promise; + + UpdateOrInsertCloudLicense(organizationSubscription: OrganizationSubscription, manager?: any): Promise; + + paymentFailedHandler(invoiceObject: any): Promise; + + subscriptionUpdateHandler(subscriptionObject: any): Promise; + + upcomingInvoiceHandler(invoiceObject: any): Promise; + + webhookCheckoutSessionCompleteHandler(paymentObject: any): Promise; + + webhookInvoicePaidHandler(invoiceObject: any): Promise; + + getOrganizationLicensePayments(organizationId?: string, subscriptionId?: string): Promise; + + createOrganizationLicensePayment( + organizationPaymentCreateObj: Partial, + manager?: any + ): Promise; + + createUpcomingInvoice( + orgInvoice: Partial, + manager?: any + ): Promise; + + updateOrganizationSubscription(updateCondition?: any, updateObject?: any): Promise; + + getRedirectUrl(user: any, paymentRedirectDto: PaymentRedirectDto): Promise; +} diff --git a/server/src/modules/organization-payments/module.ts b/server/src/modules/organization-payments/module.ts index 58bfe27e0a..9dab27c768 100644 --- a/server/src/modules/organization-payments/module.ts +++ b/server/src/modules/organization-payments/module.ts @@ -3,6 +3,7 @@ import { WhiteLabellingModule } from '../white-labelling/module'; import { EmailModule } from '@modules/email/module'; import { CrmModule } from '@modules/CRM/module'; import { SubModule } from '@modules/app/sub-module'; +import { FeatureAbilityFactory } from './ability'; @Module({}) export class OrganizationPaymentModule extends SubModule { @@ -21,7 +22,7 @@ export class OrganizationPaymentModule extends SubModule { await CrmModule.register(configs), ], controllers: [OrganizationPaymentController], - providers: [OrganizationPaymentService], + providers: [OrganizationPaymentService, FeatureAbilityFactory], exports: [], }; } diff --git a/server/src/modules/organization-payments/service.ts b/server/src/modules/organization-payments/service.ts index 22f8333d60..de031052c4 100644 --- a/server/src/modules/organization-payments/service.ts +++ b/server/src/modules/organization-payments/service.ts @@ -1,6 +1,70 @@ import { Injectable } from '@nestjs/common'; +import { IOrganizationPaymentService } from './interfaces/IService'; +import { OrganizationSubscription } from '@entities/organization_subscription.entity'; +import { OrganizationSubscriptionInvoice } from '@entities/organization_subscription_invoice.entity'; +import { ProrationDto, PortalDto, PaymentRedirectDto } from './dto'; @Injectable() -export class OrganizationPaymentService { +export class OrganizationPaymentService implements IOrganizationPaymentService { constructor() {} + getUpcomingInvoice(organizationId: string): Promise { + throw new Error('Method not implemented.'); + } + getCurrentPlan(organizationId: string): Promise { + throw new Error('Method not implemented.'); + } + updateInvoice(updateCondition?: any, updateObject?: any): Promise { + throw new Error('Method not implemented.'); + } + getProration(organizationId: string, prorationDto: ProrationDto): Promise { + throw new Error('Method not implemented.'); + } + getPortalLink(portalDto: PortalDto): Promise { + throw new Error('Method not implemented.'); + } + updateSubscription(organizationId: string, updatedSubscription: ProrationDto): Promise { + throw new Error('Method not implemented.'); + } + licenseUpgradeValidation(organizationId: string, checkParam: any, manager?: any): Promise { + throw new Error('Method not implemented.'); + } + UpdateOrInsertCloudLicense(organizationSubscription: OrganizationSubscription, manager?: any): Promise { + throw new Error('Method not implemented.'); + } + paymentFailedHandler(invoiceObject: any): Promise { + throw new Error('Method not implemented.'); + } + subscriptionUpdateHandler(subscriptionObject: any): Promise { + throw new Error('Method not implemented.'); + } + upcomingInvoiceHandler(invoiceObject: any): Promise { + throw new Error('Method not implemented.'); + } + webhookCheckoutSessionCompleteHandler(paymentObject: any): Promise { + throw new Error('Method not implemented.'); + } + webhookInvoicePaidHandler(invoiceObject: any): Promise { + throw new Error('Method not implemented.'); + } + getOrganizationLicensePayments(organizationId?: string, subscriptionId?: string): Promise { + throw new Error('Method not implemented.'); + } + createOrganizationLicensePayment( + organizationPaymentCreateObj: Partial, + manager?: any + ): Promise { + throw new Error('Method not implemented.'); + } + createUpcomingInvoice( + orgInvoice: Partial, + manager?: any + ): Promise { + throw new Error('Method not implemented.'); + } + updateOrganizationSubscription(updateCondition?: any, updateObject?: any): Promise { + throw new Error('Method not implemented.'); + } + getRedirectUrl(user: any, paymentRedirectDto: PaymentRedirectDto): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/server/src/modules/organization-payments/types/index.ts b/server/src/modules/organization-payments/types/index.ts index e11213ca8a..1047d068ab 100644 --- a/server/src/modules/organization-payments/types/index.ts +++ b/server/src/modules/organization-payments/types/index.ts @@ -4,7 +4,7 @@ import { MODULES } from '@modules/app/constants/modules'; interface Features { [FEATURE_KEY.CREATE_PORTAL_LINK]: FeatureConfig; - [FEATURE_KEY.GENERATE_PAYMENT_LINK]: FeatureConfig; + [FEATURE_KEY.GET_CURRENT_PLAN_DETAILS]: FeatureConfig; [FEATURE_KEY.GET_PRORATION]: FeatureConfig; [FEATURE_KEY.GET_REDIRECT_URL]: FeatureConfig; [FEATURE_KEY.GET_UPCOMING_INVOICE]: FeatureConfig; diff --git a/server/src/modules/organizations/controller.ts b/server/src/modules/organizations/controller.ts index 509bcd98d9..f19523b789 100644 --- a/server/src/modules/organizations/controller.ts +++ b/server/src/modules/organizations/controller.ts @@ -50,30 +50,6 @@ export class OrganizationsController implements IOrganizationsController { return; } - @InitFeature(FEATURE_KEY.WORKSPACE_ARCHIVE) - @UseGuards(JwtAuthGuard) - @Patch('/archive/:id') - async archiveOrganization( - @Body() organizationUpdateDto: OrganizationStatusUpdateDto, - @Param('id') organizationId: string, - @User() user: UserEntity - ) { - await this.organizationsService.updateOrganizationStatus(organizationId, organizationUpdateDto, user); - return; - } - - @InitFeature(FEATURE_KEY.WORKSPACE_UNARCHIVE) - @UseGuards(JwtAuthGuard) - @Patch('/unarchive/:id') - async unarchiveOrganization( - @Body() organizationUpdateDto: OrganizationStatusUpdateDto, - @Param('id') organizationId: string, - @User() user: UserEntity - ) { - await this.organizationsService.updateOrganizationStatus(organizationId, organizationUpdateDto, user); - return; - } - @InitFeature(FEATURE_KEY.CHECK_UNIQUE) @UseGuards(JwtAuthGuard, FeatureAbilityGuard) @Get('/is-unique') @@ -87,4 +63,26 @@ export class OrganizationsController implements IOrganizationsController { async checkUniqueWorkspaceName(@Query('name') name: string) { return this.organizationsService.checkWorkspaceNameUniqueness(name); } + + @InitFeature(FEATURE_KEY.WORKSPACE_ARCHIVE) + @UseGuards(JwtAuthGuard, FeatureAbilityGuard) + @Patch('/archive/:id') + async archiveOrganization( + @Body() organizationUpdateDto: OrganizationStatusUpdateDto, + @Param('id') organizationId: string, + @User() user: UserEntity + ) { + throw new Error('Not implemented'); + } + + @InitFeature(FEATURE_KEY.WORKSPACE_UNARCHIVE) + @UseGuards(JwtAuthGuard, FeatureAbilityGuard) + @Patch('/unarchive/:id') + async unarchiveOrganization( + @Body() organizationUpdateDto: OrganizationStatusUpdateDto, + @Param('id') organizationId: string, + @User() user: UserEntity + ) { + throw new Error('Not implemented'); + } } diff --git a/server/src/modules/organizations/repository.ts b/server/src/modules/organizations/repository.ts index 15862f9c21..812288b042 100644 --- a/server/src/modules/organizations/repository.ts +++ b/server/src/modules/organizations/repository.ts @@ -34,7 +34,7 @@ export class OrganizationRepository extends Repository { ...conditions, where: { ...conditions.where, slug }, }); - } catch (error) { + } catch { organization = await this.manager.findOneOrFail(Organization, { ...conditions, where: { ...conditions.where, id: slug }, @@ -87,7 +87,7 @@ export class OrganizationRepository extends Repository { where: { slug }, select, }); - } catch (error) { + } catch { organization = await manager.findOneOrFail(Organization, { where: { id: slug }, select, @@ -211,7 +211,7 @@ export class OrganizationRepository extends Repository { return await manager.findOneOrFail(Organization, { where: { isDefault: true }, }); - } catch (error) { + } catch { console.error('No default workspace in this instance'); return null; } diff --git a/server/src/modules/session/guards/organization-auth.guard.ts b/server/src/modules/session/guards/organization-auth.guard.ts index 0b74911207..41a1285c58 100644 --- a/server/src/modules/session/guards/organization-auth.guard.ts +++ b/server/src/modules/session/guards/organization-auth.guard.ts @@ -12,7 +12,7 @@ export class OrganizationAuthGuard extends AuthGuard('jwt') { if (request?.cookies['tj_auth_token']) { try { user = await super.canActivate(context); - } catch (err) { + } catch { return true; } return user; diff --git a/server/src/modules/setup-organization/types/organization-inputs.ts b/server/src/modules/setup-organization/types/organization-inputs.ts index af427cf785..3b2264ca1f 100644 --- a/server/src/modules/setup-organization/types/organization-inputs.ts +++ b/server/src/modules/setup-organization/types/organization-inputs.ts @@ -2,4 +2,5 @@ export interface OrganizationInputs { name: string; slug: string; isDefault?: boolean; + ownerId?: string; } diff --git a/server/src/modules/tooljet-db/external-modules/postgrest/postgrest.conf b/server/src/modules/tooljet-db/external-modules/postgrest/postgrest.conf.example similarity index 100% rename from server/src/modules/tooljet-db/external-modules/postgrest/postgrest.conf rename to server/src/modules/tooljet-db/external-modules/postgrest/postgrest.conf.example diff --git a/server/src/modules/tooljet-db/services/tooljet-db-table-operations.service.ts b/server/src/modules/tooljet-db/services/tooljet-db-table-operations.service.ts index 47ae973e4d..e67b9c8f87 100644 --- a/server/src/modules/tooljet-db/services/tooljet-db-table-operations.service.ts +++ b/server/src/modules/tooljet-db/services/tooljet-db-table-operations.service.ts @@ -12,7 +12,7 @@ import { } from 'typeorm'; import { InjectEntityManager } from '@nestjs/typeorm'; import { InternalTable } from 'src/entities/internal_table.entity'; -import { formatJoinsJSONBPath, formatJSONB } from 'src/helpers/utils.helper'; +import { formatJoinsJSONBPath, formatJSONB, getTooljetEdition } from 'src/helpers/utils.helper'; import { isString, isEmpty, camelCase } from 'lodash'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { @@ -30,7 +30,7 @@ import { isSQLModeDisabled, } from 'src/helpers/tooljet_db.helper'; import { OrganizationTjdbConfigurations } from 'src/entities/organization_tjdb_configurations.entity'; -const crypto = require('crypto'); +import * as crypto from 'crypto'; import { PostgrestError, TooljetDatabaseColumn, @@ -46,6 +46,7 @@ import { ConfigService } from '@nestjs/config'; import { LICENSE_FIELD, LICENSE_LIMIT, LICENSE_LIMITS_LABEL } from '@modules/licensing/constants'; import { generatePayloadForLimits } from '@modules/licensing/helper'; import { LicenseTermsService } from '@modules/licensing/interfaces/IService'; +import { TOOLJET_EDITIONS } from '@modules/app/constants'; enum AggregateFunctions { sum = 'SUM', @@ -836,11 +837,27 @@ export class TooljetDbTableOperationsService { [LICENSE_FIELD.TABLE_COUNT, LICENSE_FIELD.STATUS], organizationId ); + if (licenseTerms[LICENSE_FIELD.TABLE_COUNT] === LICENSE_LIMIT.UNLIMITED) { + return { + tablesCount: generatePayloadForLimits( + 0, + licenseTerms[LICENSE_FIELD.TABLE_COUNT], + licenseTerms[LICENSE_FIELD.STATUS], + LICENSE_LIMITS_LABEL.TABLES + ), + }; + } + const edition: TOOLJET_EDITIONS = getTooljetEdition() as TOOLJET_EDITIONS; + const tableCount = + edition === TOOLJET_EDITIONS.Cloud + ? await this.manager + .createQueryBuilder(InternalTable, 'internal_table') + .where('internal_table.organizationId = :organizationId', { organizationId }) + .getCount() + : await this.manager.createQueryBuilder(InternalTable, 'internal_table').getCount(); return { tablesCount: generatePayloadForLimits( - licenseTerms[LICENSE_FIELD.TABLE_COUNT] !== LICENSE_LIMIT.UNLIMITED - ? await this.manager.createQueryBuilder(InternalTable, 'internal_table').getCount() - : 0, + tableCount, licenseTerms[LICENSE_FIELD.TABLE_COUNT], licenseTerms[LICENSE_FIELD.STATUS], LICENSE_LIMITS_LABEL.TABLES diff --git a/server/src/modules/users/repositories/repository.ts b/server/src/modules/users/repositories/repository.ts index e3417e5e50..bbc4acaefd 100644 --- a/server/src/modules/users/repositories/repository.ts +++ b/server/src/modules/users/repositories/repository.ts @@ -99,7 +99,7 @@ export class UserRepository extends Repository { if (existingUser) { Object.assign(existingUser, user); - return manager.save(User, existingUser); + return manager.update(User, { id: existingUser.id }, user); } else { const newUser = manager.create(User, user); return manager.save(User, newUser); diff --git a/server/src/modules/white-labelling/Interfaces/IController.ts b/server/src/modules/white-labelling/Interfaces/IController.ts index 955f94e624..91038a06d6 100644 --- a/server/src/modules/white-labelling/Interfaces/IController.ts +++ b/server/src/modules/white-labelling/Interfaces/IController.ts @@ -2,15 +2,15 @@ import { User } from '@entities/user.entity'; import { UpdateWhiteLabellingDto } from '@modules/white-labelling/dto'; export interface IWhiteLabellingController { - get(req: any): Promise; // Method to fetch white labeling settings + getInstanceWhiteLabelling(): Promise; // Method to fetch instance level white labeling settings - update(updateWhiteLabellingDto: UpdateWhiteLabellingDto, user: User): Promise; // Method to update white labeling settings + updateInstanceWhiteLabelling(updateWhiteLabellingDto: UpdateWhiteLabellingDto, user: User): Promise; // Method to update instance level white labeling settings - getWorkspaceSettings(workspaceId: string): Promise; // Method to get specific white labeling settings for an organization + getWorkspaceWhiteLabelling(organizationId: string): Promise; // Method to get workspace level white labeling settings - updateWorkspaceSettings( + updateWorkspaceWhiteLabelling( organizationId: string, updateWhiteLabellingDto: UpdateWhiteLabellingDto, user: User - ): Promise; // Method to update white labeling settings for a specific organization + ): Promise; // Method to update workspace level white labeling settings } diff --git a/server/src/modules/white-labelling/ability/index.ts b/server/src/modules/white-labelling/ability/index.ts index 17973b3286..385160a63b 100644 --- a/server/src/modules/white-labelling/ability/index.ts +++ b/server/src/modules/white-labelling/ability/index.ts @@ -21,9 +21,9 @@ export class FeatureAbilityFactory extends AbilityFactory const { superAdmin, isAdmin } = userPermissions; if (isAdmin || superAdmin) { - can([FEATURE_KEY.UPDATE, FEATURE_KEY.UPDATE_WORKSPACE_SETTINGS], WhiteLabelling); + can([FEATURE_KEY.UPDATE, FEATURE_KEY.UPDATE_ORGANIZATION_WHITE_LABELS], WhiteLabelling); } // All users can perform these actions - can([FEATURE_KEY.GET, FEATURE_KEY.GET_WORKSPACE_SETTINGS], WhiteLabelling); + can([FEATURE_KEY.GET, FEATURE_KEY.GET_ORGANIZATION_WHITE_LABELS], WhiteLabelling); } } diff --git a/server/src/modules/white-labelling/constant/feature.ts b/server/src/modules/white-labelling/constant/feature.ts index e020c7b1ef..c519f0bd9a 100644 --- a/server/src/modules/white-labelling/constant/feature.ts +++ b/server/src/modules/white-labelling/constant/feature.ts @@ -5,9 +5,9 @@ import { FEATURE_KEY } from '.'; export const FEATURES: FeaturesConfig = { [MODULES.WHITE_LABELLING]: { - [FEATURE_KEY.GET]: {}, + [FEATURE_KEY.GET]: { isPublic: true }, [FEATURE_KEY.UPDATE]: { license: LICENSE_FIELD.WHITE_LABEL }, - [FEATURE_KEY.GET_WORKSPACE_SETTINGS]: { isPublic: true }, - [FEATURE_KEY.UPDATE_WORKSPACE_SETTINGS]: { license: LICENSE_FIELD.WHITE_LABEL }, + [FEATURE_KEY.GET_ORGANIZATION_WHITE_LABELS]: { license: LICENSE_FIELD.WHITE_LABEL }, + [FEATURE_KEY.UPDATE_ORGANIZATION_WHITE_LABELS]: { license: LICENSE_FIELD.WHITE_LABEL }, }, }; diff --git a/server/src/modules/white-labelling/constant/index.ts b/server/src/modules/white-labelling/constant/index.ts index 1ba03ac5da..03e1228cf1 100644 --- a/server/src/modules/white-labelling/constant/index.ts +++ b/server/src/modules/white-labelling/constant/index.ts @@ -7,6 +7,7 @@ export const DEFAULT_WHITE_LABELLING_SETTINGS = { export enum FEATURE_KEY { GET = 'GET', // For the get method (fetching general white-labelling info) UPDATE = 'UPDATE', // For the update method (updating white-labelling settings) - GET_WORKSPACE_SETTINGS = 'GET_WORKSPACE_SETTINGS', // For the getCloudSettings method - UPDATE_WORKSPACE_SETTINGS = 'UPDATE_WORKSPACE_SETTINGS', // For the updateCloudSettings method + GET_ORGANIZATION_WHITE_LABELS = 'GET_ORGANIZATION_WHITE_LABELS', // For the getCloudSettings method + UPDATE_ORGANIZATION_WHITE_LABELS = 'UPDATE_WORKSPACE_SETTINGS_CLOUD', // For the updateCloudSettings method } +// Review the name change : name looks ambigous for cloud white labelling. diff --git a/server/src/modules/white-labelling/controller.ts b/server/src/modules/white-labelling/controller.ts index cbe4b70198..67b1e1ecbe 100644 --- a/server/src/modules/white-labelling/controller.ts +++ b/server/src/modules/white-labelling/controller.ts @@ -1,4 +1,4 @@ -import { Controller, Body, Put, Get, Param, Query } from '@nestjs/common'; +import { Controller, Body, Put, Get, Param } from '@nestjs/common'; import { UpdateWhiteLabellingDto } from './dto'; import { IWhiteLabellingController } from './Interfaces/IController'; import { NotFoundException } from '@nestjs/common'; @@ -7,6 +7,8 @@ import { InitFeature } from '@modules/app/decorators/init-feature.decorator'; import { MODULES } from '@modules/app/constants/modules'; import { FEATURE_KEY } from './constant'; import { WhiteLabellingService } from './service'; +import { User as UserEntity } from '@entities/user.entity'; +import { User } from '@modules/app/decorators/user.decorator'; @Controller('white-labelling') @InitModule(MODULES.WHITE_LABELLING) @@ -15,25 +17,29 @@ export class WhiteLabellingController implements IWhiteLabellingController { @Get() @InitFeature(FEATURE_KEY.GET) - async get(@Query('organizationId') organizationId: string) { - return this.whiteLabellingService.getProcessedSettings(null); + async getInstanceWhiteLabelling() { + const formattedSettings = await this.whiteLabellingService.getProcessedSettings(null); + return formattedSettings; } @Put() @InitFeature(FEATURE_KEY.UPDATE) - async update(@Body() updateWhiteLabellingDto: UpdateWhiteLabellingDto) { + async updateInstanceWhiteLabelling( + @Body() updateWhiteLabellingDto: UpdateWhiteLabellingDto, + @User() user: UserEntity + ) { throw new NotFoundException(); } - @Get('/:workspaceId') - @InitFeature(FEATURE_KEY.GET_WORKSPACE_SETTINGS) - async getWorkspaceSettings(req: any) { + @Get('/:organizationId') + @InitFeature(FEATURE_KEY.GET_ORGANIZATION_WHITE_LABELS) + async getWorkspaceWhiteLabelling(req: any) { throw new NotFoundException(); } @Put('/:organizationId') - @InitFeature(FEATURE_KEY.UPDATE_WORKSPACE_SETTINGS) - async updateWorkspaceSettings( + @InitFeature(FEATURE_KEY.UPDATE_ORGANIZATION_WHITE_LABELS) + async updateWorkspaceWhiteLabelling( @Param('organizationId') organizationId: string, @Body() updateWhiteLabellingDto: UpdateWhiteLabellingDto, user: any diff --git a/server/src/modules/white-labelling/guards/verifyOrgId.guard.ts b/server/src/modules/white-labelling/guards/verifyOrgId.guard.ts index d40f86a8e3..c613b3e529 100644 --- a/server/src/modules/white-labelling/guards/verifyOrgId.guard.ts +++ b/server/src/modules/white-labelling/guards/verifyOrgId.guard.ts @@ -8,7 +8,7 @@ export class OrganizationIdSlugValidationGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const workspaceId = request?.query['organizationId']; + const workspaceId = request?.params.organizationId; let organization: Organization; try { diff --git a/server/src/modules/white-labelling/types/index.ts b/server/src/modules/white-labelling/types/index.ts index 235063b0c0..ad42bad385 100644 --- a/server/src/modules/white-labelling/types/index.ts +++ b/server/src/modules/white-labelling/types/index.ts @@ -5,8 +5,8 @@ import { FEATURE_KEY } from '../constant'; interface Features { [FEATURE_KEY.GET]: FeatureConfig; [FEATURE_KEY.UPDATE]: FeatureConfig; - [FEATURE_KEY.GET_WORKSPACE_SETTINGS]: FeatureConfig; - [FEATURE_KEY.UPDATE_WORKSPACE_SETTINGS]: FeatureConfig; + [FEATURE_KEY.GET_ORGANIZATION_WHITE_LABELS]: FeatureConfig; + [FEATURE_KEY.UPDATE_ORGANIZATION_WHITE_LABELS]: FeatureConfig; } export interface FeaturesConfig { diff --git a/server/tsconfig.json b/server/tsconfig.json index 68f4ca18bc..cfcc76168c 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,7 +1,10 @@ { "compilerOptions": { "moduleResolution": "node", - "types": ["node", "rxjs"], + "types": [ + "node", + "rxjs" + ], "resolveJsonModule": true, "module": "commonjs", "declaration": true, @@ -17,22 +20,25 @@ "incremental": true, "skipLibCheck": true, "paths": { - "@ee/*": ["ee/*"], - "@apps/*": ["ee/apps/*"], - "@entities/*": ["src/entities/*"], - "@services/*": ["src/services/*"], - "@controllers/*": ["src/controllers/*"], - "@repositories/*": ["src/repositories/*"], - "@dto/*": ["src/dto/*"], - "@modules/*": ["src/modules/*"], - "@helpers/*": ["src/helpers/*"], - "@instance-settings/*": ["ee/instance-settings/*"], + "@ee/*": [ + "ee/*" + ], + "@entities/*": [ + "src/entities/*" + ], + "@dto/*": [ + "src/dto/*" + ], + "@modules/*": [ + "src/modules/*" + ], + "@helpers/*": [ + "src/helpers/*" + ], }, }, "exclude": [ "node_modules", "dist", - "src/services/ignored", - "ee/ignored" ] -} +} \ No newline at end of file