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 <gsmithun4@gmail.com>

* 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 <gsmithun4@gmail.com>

* 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 <ajith.jaban@gmail.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: rohanlahori <rohanlahori99@gmail.com>
Co-authored-by: Adish M <adish.madhu@gmail.com>
Co-authored-by: Rudra deep Biswas <rudra21ultra@gmail.com>
This commit is contained in:
Midhun G S 2025-07-09 22:36:41 +05:30 committed by GitHub
parent 7b0b5ca13d
commit 0c5ab3484c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
140 changed files with 1883 additions and 1799 deletions

View file

@ -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

2
.nvmrc
View file

@ -1 +1 @@
v18.18.2
v22.15.1

View file

@ -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,

View file

@ -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

View file

@ -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 = {

View file

@ -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(

View file

@ -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(

View file

@ -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();

View file

@ -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();
});
});

View file

@ -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);

View file

@ -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);

View file

@ -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);
};

View file

@ -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 = ({
</div>
</div>
{orgGit?.org_git?.is_enabled && (
{isGitSyncEnabled && (
<div className="commit-changes" style={{ marginTop: '-1rem', marginBottom: '2rem' }}>
<div>
<input

View file

@ -2,7 +2,7 @@
const initialState = {
activeOrganizationId: null,
whiteLabelText: 'ToolJet',
whiteLabelLogo: null,
whiteLabelLogo: 'assets/images/tj-logo.svg', //Default whitelbeling logo
whiteLabelFavicon: null,
loadingWhiteLabelDetails: true,
isWhiteLabelDetailsFetched: false,

View file

@ -85,12 +85,6 @@ export const AppMenu = function AppMenu({
)}
{canUpdateApp && canCreateApp && appType !== 'workflow' && (
<>
{appType !== 'workflow' && (
<Field
text={t('homePage.appCard.cloneApp', 'Clone app')}
onClick={() => openAppActionModal('clone-app')}
/>
)}
<Field
text={
appType === 'workflow' ? 'Clone workflow' : appType === 'module' ? 'Clone module' : 'Clone app'

View file

@ -16,7 +16,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
import FolderSkeleton from '@/_ui/FolderSkeleton/FolderSkeleton';
import { Button } from '@/components/ui/Button/Button';
export const Folders = function Folders({
export const Folders = function Folders ({
folders,
foldersLoading,
currentFolder,
@ -74,7 +74,7 @@ export const Folders = function Folders({
setFilteredData(filtered);
};
function saveFolder() {
function saveFolder () {
const newName = newFolderName?.trim();
if (!newName) {
setErrorText("Folder name can't be empty");
@ -103,7 +103,7 @@ export const Folders = function Folders({
return `All ${appType === 'workflow' ? 'workflows' : appType === 'module' ? 'modules' : 'apps'}`;
};
function handleFolderChange(folder) {
function handleFolderChange (folder) {
if (_.isEmpty(folder)) {
setActiveFolder({});
} else {
@ -115,31 +115,30 @@ export const Folders = function Folders({
updateFolderQuery(folder?.name);
}
function updateFolderQuery(name) {
function updateFolderQuery (name) {
const search = `${name ? `?folder=${name}` : ''}`;
navigate(
{
pathname: `/${getWorkspaceId()}${
appType === 'workflow' ? '/workflows' : appType === 'module' ? '/modules' : ''
}`,
pathname: `/${getWorkspaceId()}${appType === 'workflow' ? '/workflows' : appType === 'module' ? '/modules' : ''
}`,
search,
},
{ replace: true }
);
}
function deleteFolder(folder) {
function deleteFolder (folder) {
setShowDeleteConfirmation(true);
setDeletingFolder(folder);
}
function updateFolder(folder) {
function updateFolder (folder) {
setNewFolderName(folder.name);
setShowUpdateForm(true);
setUpdatingFolder(folder);
}
function executeDeletion() {
function executeDeletion () {
setDeletionStatus(true);
folderService
.deleteFolder(deletingFolder.id)
@ -157,12 +156,12 @@ export const Folders = function Folders({
});
}
function cancelDeleteDialog() {
function cancelDeleteDialog () {
setShowDeleteConfirmation(false);
setDeletingFolder(null);
}
function executeEditFolder() {
function executeEditFolder () {
const folderName = newFolderName?.trim();
if (folderName === updatingFolder?.name) {
setUpdationStatus(false);
@ -213,7 +212,7 @@ export const Folders = function Folders({
showUpdateForm ? setShowUpdateForm(false) : setShowForm(false);
};
function handleClose() {
function handleClose () {
setShowInput(false);
setFilteredData(folders);
}
@ -269,7 +268,7 @@ export const Folders = function Folders({
onClick={() => {
setShowInput(true);
}}
data-cy="create-new-folder-button"
data-cy="folder-search-icon"
>
<SolidIcon
name="search"
@ -311,9 +310,9 @@ export const Folders = function Folders({
{appType === 'module'
? 'All modules'
: t(
`${appType === 'workflow' ? 'workflowsDashboard' : 'homePage'}.foldersSection.allApplications`,
'All apps'
)}
`${appType === 'workflow' ? 'workflowsDashboard' : 'homePage'}.foldersSection.allApplications`,
'All apps'
)}
</a>
</div>
)}

View file

@ -1655,11 +1655,6 @@ class HomePageComponent extends React.Component {
<div className="w-100 mb-5 container home-page-content-container">
{featuresLoaded && !isLoading ? (
<>
<LicenseBanner
classes="mt-3"
limits={featureAccess}
type={featureAccess?.licenseStatus?.licenseType}
/>
<AppTypeTab
appType={this.props.appType}
navigate={this.props.navigate}

View file

@ -14,9 +14,9 @@ const LegalReasonsErrorModal = ({
body,
showFooter = true,
toggleModal,
actionButtonAdmin,
}) => {
const [isOpen, setShowModal] = useState(propShowModal);
const currentUser = authenticationService.currentSessionValue;
const handleClose = () => {
setShowModal(false);
toggleModal && toggleModal();
@ -63,7 +63,7 @@ const LegalReasonsErrorModal = ({
<Button className="cancel-btn" onClick={handleClose} data-cy="cancel-button">
Cancel
</Button>
{currentUser?.super_admin && (
{actionButtonAdmin && (
<Button className="upgrade-btn" autoFocus onClick={() => {}}>
<a
style={{ color: 'white', textDecoration: 'none' }}

View file

@ -25,109 +25,109 @@ import { fetchWhiteLabelDetails } from '@/_helpers/white-label/whiteLabelling';
export const authorizeWorkspace = () => {
/* Default APIs */
const workspaceIdOrSlug = getWorkspaceIdOrSlugFromURL();
fetchWhiteLabelDetails(workspaceIdOrSlug).finally(() => {
if (!isThisExistedRoute()) {
updateCurrentSession({
triggeredOnce: true,
});
const isApplicationsPath =
getPathname(null, true).startsWith('/applications/') || getPathname(null, true).startsWith('/embed-apps/');
const appId = isApplicationsPath ? getPathname().split('/')[2] : null;
/* CASE-1 */
sessionService
.validateSession(appId, workspaceIdOrSlug)
.then(
({
current_organization_id,
current_organization_slug,
no_workspace_attached_in_the_session: noWorkspaceAttachedInTheSession,
is_all_workspaces_archived: isAllWorkspacesArchived,
is_onboarding_completed: isOnboardingCompleted,
is_first_user_onboarding_completed: isFirstUserOnboardingCompleted,
consulation_banner_date,
}) => {
if (!isFirstUserOnboardingCompleted) {
const subpath = getSubpath();
const path = subpath ? `${subpath}/setup` : '/setup';
window.location.href = path;
} else if (!isOnboardingCompleted) {
// const subpath = getSubpath();
// const path = subpath ? `${subpath}/confirm` : '/confirm';
// window.location.href
}
// fetchWhiteLabelDetails(workspaceIdOrSlug).finally(() => {
if (!isThisExistedRoute()) {
updateCurrentSession({
triggeredOnce: true,
});
const isApplicationsPath =
getPathname(null, true).startsWith('/applications/') || getPathname(null, true).startsWith('/embed-apps/');
const appId = isApplicationsPath ? getPathname().split('/')[2] : null;
/* CASE-1 */
sessionService
.validateSession(appId, workspaceIdOrSlug)
.then(
({
current_organization_id,
current_organization_slug,
no_workspace_attached_in_the_session: noWorkspaceAttachedInTheSession,
is_all_workspaces_archived: isAllWorkspacesArchived,
is_onboarding_completed: isOnboardingCompleted,
is_first_user_onboarding_completed: isFirstUserOnboardingCompleted,
consulation_banner_date,
}) => {
if (!isFirstUserOnboardingCompleted) {
const subpath = getSubpath();
const path = subpath ? `${subpath}/setup` : '/setup';
window.location.href = path;
} else if (!isOnboardingCompleted) {
// const subpath = getSubpath();
// const path = subpath ? `${subpath}/confirm` : '/confirm';
// window.location.href
}
if (window.location.pathname !== `${getSubpath() ?? ''}/switch-workspace`) {
if (isAllWorkspacesArchived) {
/* All workspaces are archived by the super admin. lets logout the user */
sessionService.logout();
} else {
updateCurrentSession({
noWorkspaceAttachedInTheSession,
authentication_status: true,
consulation_banner_date,
});
if (noWorkspaceAttachedInTheSession) {
/*
if (window.location.pathname !== `${getSubpath() ?? ''}/switch-workspace`) {
if (isAllWorkspacesArchived) {
/* All workspaces are archived by the super admin. lets logout the user */
sessionService.logout();
} else {
updateCurrentSession({
noWorkspaceAttachedInTheSession,
authentication_status: true,
consulation_banner_date,
});
if (noWorkspaceAttachedInTheSession) {
/*
User just signed up after the invite flow and doesn't have any active workspace.
- From useSessionManagement hook we will be redirecting the user to an error page.
*/
return;
}
/*CASE-2*/
authorizeUserAndHandleErrors(current_organization_id, current_organization_slug);
return;
}
} else {
updateCurrentSession({
current_organization_id,
});
/*CASE-2*/
authorizeUserAndHandleErrors(current_organization_id, current_organization_slug);
}
}
)
.catch((error) => {
const isDesiredStatusCode =
(error && error?.data?.statusCode == 422) ||
error?.data?.statusCode == 404 ||
error?.data?.statusCode == 400;
if (isDesiredStatusCode) {
const isWorkspaceArchived =
error?.data?.statusCode == 400 && error?.data?.message == ERROR_TYPES.WORKSPACE_ARCHIVED;
if (isWorkspaceArchived) {
const subpath = getSubpath();
let path = subpath ? `${subpath}/switch-workspace` : `/switch-workspace`;
if (appId) {
path = 'app-url-archived';
} else {
path += '-archived';
}
window.location = path;
} else if (appId) {
/* If the user is trying to load the app viewer and the app id / slug not found */
redirectToErrorPage(ERROR_TYPES.INVALID);
} else if (error?.data?.statusCode == 422) {
if (isThisWorkspaceLoginPage()) {
return redirectToErrorPage(ERROR_TYPES.INVALID);
}
redirectToErrorPage(ERROR_TYPES.UNKNOWN);
} else {
const subpath = getSubpath();
window.location = subpath ? `${subpath}${'/switch-workspace'}` : '/switch-workspace';
}
}
if (!isApplicationsPath) {
/* CASE-3 */
} else {
updateCurrentSession({
authentication_status: false,
});
} else if (isApplicationsPath) {
/* CASE-4 */
updateCurrentSession({
authentication_failed: true,
load_app: true,
current_organization_id,
});
}
});
}
});
fetchWhiteLabelDetails();
}
)
.catch((error) => {
fetchWhiteLabelDetails();
const isDesiredStatusCode =
(error && error?.data?.statusCode == 422) || error?.data?.statusCode == 404 || error?.data?.statusCode == 400;
if (isDesiredStatusCode) {
const isWorkspaceArchived =
error?.data?.statusCode == 400 && error?.data?.message == ERROR_TYPES.WORKSPACE_ARCHIVED;
if (isWorkspaceArchived) {
const subpath = getSubpath();
let path = subpath ? `${subpath}/switch-workspace` : `/switch-workspace`;
if (appId) {
path = 'app-url-archived';
} else {
path += '-archived';
}
window.location = path;
} else if (appId) {
/* If the user is trying to load the app viewer and the app id / slug not found */
redirectToErrorPage(ERROR_TYPES.INVALID);
} else if (error?.data?.statusCode == 422) {
if (isThisWorkspaceLoginPage()) {
return redirectToErrorPage(ERROR_TYPES.INVALID);
}
redirectToErrorPage(ERROR_TYPES.UNKNOWN);
} else {
const subpath = getSubpath();
window.location = subpath ? `${subpath}${'/switch-workspace'}` : '/switch-workspace';
}
}
if (!isApplicationsPath) {
/* CASE-3 */
updateCurrentSession({
authentication_status: false,
});
} else if (isApplicationsPath) {
/* CASE-4 */
updateCurrentSession({
authentication_failed: true,
load_app: true,
});
}
});
}
// });
};
const isThisExistedRoute = () => {

View file

@ -7,6 +7,11 @@ export const whiteLabellingOptions = {
WHITE_LABEL_LOGO: 'white_label_logo',
WHITE_LABEL_FAVICON: 'white_label_favicon',
};
export const defaultWhiteLabellingSettings = {
WHITE_LABEL_LOGO: 'assets/images/tj-logo.svg',
WHITE_LABEL_TEXT: 'ToolJet',
WHITE_LABEL_FAVICON: 'assets/images/logo.svg',
};
export function retrieveWhiteLabelFavicon() {
const { whiteLabelFavicon } = useWhiteLabellingStore.getState();
@ -137,8 +142,14 @@ export async function resetToDefaultWhiteLabels() {
// Check if current settings match the default values
export function checkWhiteLabelsDefaultState() {
const { isDefaultWhiteLabel } = useWhiteLabellingStore.getState();
return isDefaultWhiteLabel;
const whiteLabelText = retrieveWhiteLabelText();
const whiteLabelFavicon = retrieveWhiteLabelFavicon();
const whiteLabelLogo = retrieveWhiteLabelLogo();
return (
(!whiteLabelText || whiteLabelText === defaultWhiteLabellingSettings.WHITE_LABEL_TEXT) &&
(!whiteLabelLogo || whiteLabelLogo === defaultWhiteLabellingSettings.WHITE_LABEL_LOGO) &&
(!whiteLabelFavicon || whiteLabelFavicon === defaultWhiteLabellingSettings.WHITE_LABEL_FAVICON)
);
}
export const pageTitles = {

View file

@ -1,5 +1,6 @@
import config from 'config';
import { authHeader, handleResponseWithoutValidation } from '@/_helpers';
import { fetchEdition } from '@/modules/common/helpers/utils';
export const whiteLabellingService = {
get,
@ -13,11 +14,16 @@ function get(organizationId = null) {
headers: headers,
credentials: 'include',
};
const edition = fetchEdition();
const orgId = headers['tj-workspace-id'];
console.log(headers, 'headers');
return fetch(`${config.apiUrl}/white-labelling?organizationId=${organizationId || orgId}`, requestOptions).then(
handleResponseWithoutValidation
);
console.log('organization-id', organizationId, orgId);
if (edition === 'cloud') {
return fetch(`${config.apiUrl}/white-labelling/${organizationId || orgId}`, requestOptions).then(
handleResponseWithoutValidation
);
}
// For CE AND EE, make API call without organization ID parameter
return fetch(`${config.apiUrl}/white-labelling`, requestOptions).then(handleResponseWithoutValidation);
}
function update(settings) {
@ -29,7 +35,11 @@ function update(settings) {
body: JSON.stringify(settings),
};
const organizationId = headers['tj-workspace-id'];
return fetch(`${config.apiUrl}/white-labelling/${organizationId}`, requestOptions).then(
handleResponseWithoutValidation
);
const edition = fetchEdition();
if (edition === 'cloud') {
return fetch(`${config.apiUrl}/white-labelling/${organizationId}`, requestOptions).then(
handleResponseWithoutValidation
);
}
return fetch(`${config.apiUrl}/white-labelling`, requestOptions).then(handleResponseWithoutValidation);
}

View file

@ -492,6 +492,7 @@
background: rgba(240, 244, 255, 1);
color: rgba(62, 99, 221, 1);
align-self: baseline;
height: auto;
&.valid-status {
border-radius: 100px;

View file

@ -9431,10 +9431,9 @@ tbody {
}
.tj-version {
margin-right: 44px;
display: flex;
align-items: center;
color: var(--slate9);
color: var(--text-disabled);
}
@ -9505,6 +9504,20 @@ tbody {
color: var(-slate12) !important;
}
.tj-dashboard-header-wrap {
background-color: var(--page-default);
padding-top: 8px;
padding-bottom: 8px;
padding-left: 32px;
padding-right: 32px;
height: 48px;
border-bottom: 1px solid var(--slate5);
@media only screen and (max-width: 768px) {
border-bottom: none;
}
}
.dashboard-breadcrumb-header-name:hover {
text-decoration: none !important;
}

View file

@ -9,6 +9,7 @@ const routes = [
{ path: '/:worspace_id', breadcrumb: 'Applications' },
{ path: '/:workspace_id/modules', breadcrumb: 'All modules' },
{ path: '/:worspace_id/database', breadcrumb: 'Tables', props: { dataCy: 'tables-page-header' } },
{ path: '/:workspace_id/modules', breadcrumb: 'All modules' },
{ path: '/workspace-settings', breadcrumb: 'Workspace settings' },
{ path: '/:worpsace_id/audit-logs', breadcrumb: ' ' },
{ path: '/data-sources', breadcrumb: 'Data sources' },
@ -26,120 +27,31 @@ const routes = [
{ path: '/integrations/installed', breadcrumb: 'Integrations' },
{ path: '/integrations/marketplace', breadcrumb: 'Integrations' },
];
import useBreadcrumbs from 'use-react-router-breadcrumbs';
import { decodeEntities } from '@/_helpers/utils';
export const Breadcrumbs = ({ breadcrumbs, darkMode }) => {
const { updateSidebarNAV } = useContext(BreadCrumbContext);
export const Breadcrumbs = ({ darkMode, dataCy }) => {
const { sidebarNav } = useContext(BreadCrumbContext);
const breadcrumbs = useBreadcrumbs(routes, { excludePaths: ['/'] });
const location = useLocation();
const search = location.search || '';
// Generate breadcrumbs from current location if not provided
const generatedBreadcrumbs =
breadcrumbs ||
(() => {
const pathname = location.pathname;
const pathParts = pathname.split('/').filter(Boolean);
// Special handling for apps and modules pages
if (pathParts.length >= 1) {
const workspaceId = pathParts[0];
// Apps page is at root workspace path (/:workspaceId)
if (pathParts.length === 1) {
return [
{
breadcrumb: 'Applications',
key: `/${workspaceId}`,
props: {},
},
{
breadcrumb: 'All apps',
key: `/${workspaceId}`,
props: {},
},
];
}
// Modules page is at /:workspaceId/modules
if (pathParts.length === 2 && pathParts[1] === 'modules') {
return [
{
breadcrumb: 'Applications',
key: `/${workspaceId}`,
props: {},
},
{
breadcrumb: 'All modules',
key: `/${workspaceId}/modules`,
props: {},
},
];
}
}
// Find matching route from the routes array
const matchingRoute = routes.find((route) => {
const routePathParts = route.path.split('/').filter(Boolean);
if (routePathParts.length !== pathParts.length) return false;
return routePathParts.every((part, index) => {
return part.startsWith(':') || part === pathParts[index];
});
});
if (matchingRoute) {
return [
{
breadcrumb: matchingRoute.breadcrumb,
key: matchingRoute.path,
props: matchingRoute.props || {},
},
];
}
// Fallback: use the last path part as breadcrumb
const lastPart = pathParts[pathParts.length - 1];
if (lastPart) {
return [
{
breadcrumb: lastPart.charAt(0).toUpperCase() + lastPart.slice(1).replace(/-/g, ' '),
key: pathname,
props: {},
},
];
}
return [];
})();
const breadcrumbsLength = generatedBreadcrumbs?.length || 0;
let parent = null;
let current = null;
if (breadcrumbsLength >= 2) {
parent = generatedBreadcrumbs[breadcrumbsLength - 2]; // Applications
current = generatedBreadcrumbs[breadcrumbsLength - 1]; // All modules (or whatever)
} else if (breadcrumbsLength === 1) {
parent = generatedBreadcrumbs[0]; // Applications
current = null; // fallback to sidebarNav
}
return (
<ol className="breadcrumb breadcrumb-arrows">
<div
key={parent?.key || 'breadcrumb'}
className="tj-dashboard-header-title-wrap"
data-cy={parent?.props?.dataCy ?? ''}
>
{parent && <p className="tj-text-xsm">{parent.breadcrumb}</p>}
{(current || updateSidebarNAV) && <SolidIcon name="cheveronright" fill={darkMode ? '#FDFDFE' : '#131620'} />}
{(current || updateSidebarNAV) && (
<li className="breadcrumb-item font-weight-500" data-cy="breadcrumb-page-title">
{current?.breadcrumb || updateSidebarNAV}
</li>
)}
</div>
{breadcrumbs.map(({ breadcrumb, beta }, i) => {
if (i == 1 || breadcrumbs?.length == 1) {
return (
<div key={breadcrumb.key} className="tj-dashboard-header-title-wrap" data-cy={dataCy ?? ''}>
<p className=" tj-text-xsm ">{breadcrumb}</p>
{sidebarNav?.length > 0 && <SolidIcon name="cheveronright" fill={darkMode ? '#FDFDFE' : '#131620'} />}
<li className="breadcrumb-item font-weight-500" data-cy="breadcrumb-page-title">
{' '}
{sidebarNav && decodeEntities(sidebarNav)}
</li>
</div>
);
}
})}
</ol>
);
};

View file

@ -2,9 +2,9 @@ import React from 'react';
import cx from 'classnames';
import { Breadcrumbs } from '../Breadcrumbs';
import { useLocation } from 'react-router-dom';
import LicenseBanner from '@/modules/common/components/LicenseBanner';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { ToolTip } from '@/_components';
import LicenseBanner from '@/modules/common/components/LicenseBanner';
function Header({
featureAccess,
@ -109,7 +109,7 @@ function Header({
iconWidth="14"
size="md"
onClick={toggleCollapsibleSidebar}
></ButtonSolid>
/>
</div>
</ToolTip>
)}
@ -140,20 +140,21 @@ function Header({
iconWidth="14"
size="md"
onClick={toggleCollapsibleSidebar}
></ButtonSolid>
/>
</div>
</ToolTip>
)}
<div className="app-header-label" data-cy="app-header-label">
<div className="app-header-label tw-flex tw-items-center " data-cy="app-header-label">
<Breadcrumbs darkMode={darkMode} />
</div>
<div
className={cx('ms-auto tj-version tj-text-xsm', {
className={cx('tw-ml-auto tj-version tj-text-xsm tw-flex tw-items-center tw-gap-3', {
'color-muted-darkmode': darkMode,
'color-disabled': !darkMode,
})}
data-cy="version-label"
>
<LicenseBanner limits={featureAccess} showNavBarActions={true} />
Version {currentVersion}
</div>
</div>

View file

@ -0,0 +1,14 @@
import React from 'react';
const PremiumPlan = ({ width = '41', height = '41' }) => (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 24 24" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 2C12.2173 2.00004 12.4287 2.07086 12.6021 2.20174C12.7756 2.33261 12.9017 2.51642 12.9614 2.72536L14.0454 6.52016C14.2788 7.33742 14.7167 8.08168 15.3177 8.68268C15.9187 9.28367 16.6629 9.72154 17.4802 9.95495L21.275 11.039C21.4838 11.0988 21.6675 11.225 21.7982 11.3984C21.9289 11.5719 21.9996 11.7832 21.9996 12.0004C21.9996 12.2176 21.9289 12.4289 21.7982 12.6023C21.6675 12.7757 21.4838 12.9019 21.275 12.9617L17.4802 14.0458C16.6629 14.2792 15.9187 14.7171 15.3177 15.318C14.7167 15.919 14.2788 16.6633 14.0454 17.4806L12.9614 21.2754C12.9016 21.4842 12.7754 21.6678 12.6019 21.7985C12.4285 21.9293 12.2172 22 12 22C11.7828 22 11.5715 21.9293 11.3981 21.7985C11.2246 21.6678 11.0985 21.4842 11.0386 21.2754L9.9546 17.4806C9.72119 16.6633 9.28331 15.919 8.68232 15.318C8.08133 14.7171 7.33706 14.2792 6.51981 14.0458L2.725 12.9617C2.5162 12.9019 2.33255 12.7757 2.20182 12.6023C2.07108 12.4289 2.00037 12.2176 2.00037 12.0004C2.00037 11.7832 2.07108 11.5719 2.20182 11.3984C2.33255 11.225 2.5162 11.0988 2.725 11.039L6.51981 9.95495C7.33706 9.72154 8.08133 9.28367 8.68232 8.68268C9.28331 8.08168 9.72119 7.33742 9.9546 6.52016L11.0386 2.72536C11.0983 2.51642 11.2244 2.33261 11.3979 2.20174C11.5713 2.07086 11.7827 2.00004 12 2Z"
fill="#FCA23F"
/>
</svg>
);
export default PremiumPlan;

View file

@ -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 <Contactv3 {...props} />;
case 'premium-logo':
return <PremiumLogo {...props} />;
case 'premium-plan':
return <PremiumPlan {...props} />;
case 'calendar-icon':
return <CalendarIcon {...props} />;
case 'calendar-small':

View file

@ -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}
/>
<div style={{ paddingTop: 48 }}>{children}</div>
</div>

View file

@ -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 {
)}
</div>
</div>
{!_.isEmpty(featureAccess) && !isFeatureEnabled && (
{(!isValidLicense || planType === 'trial') && featureAccess && (
<LicenseBanner
style={{ alignSelf: 'flex-end', margin: '0px !important' }}
limits={featureAccess}

View file

@ -10,6 +10,7 @@ import SolidIcon from '@/_ui/Icon/SolidIcons';
import { useAppDataStore } from '@/_stores/appDataStore';
import { shallow } from 'zustand/shallow';
import { checkIfToolJetCloud } from '@/_helpers/utils';
import { fetchEdition } from '@/modules/common/helpers/utils';
function BaseSettingsMenu({
darkMode,
@ -21,6 +22,7 @@ function BaseSettingsMenu({
hideMarketPlaceMenuItem: false,
},
}) {
const edition = fetchEdition();
const [showOverlay, setShowOverlay] = useState(false);
const { tooljetVersion } = useAppDataStore(
(state) => ({
@ -77,7 +79,6 @@ function BaseSettingsMenu({
featureAccess,
checkForUnsavedChanges,
});
return (
<div className={`settings-card tj-text card ${darkMode ? 'dark-theme' : ''}`}>
{/* 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 && (
<Link

View file

@ -12,6 +12,7 @@ import OverflowTooltip from '@/_components/OverflowTooltip';
import { NoActiveWorkspaceModal } from './components/NoActiveWorkspaceModal';
import Spinner from 'react-bootstrap/Spinner';
import { ToolTip } from '@/_components/ToolTip';
import { fetchEdition } from '../../helpers/utils';
const UsersTable = ({
isLoading,
users,
@ -56,6 +57,7 @@ const UsersTable = ({
setSelectedUser(user);
setIsResetPasswordModalVisible(true);
};
const edition = fetchEdition();
return (
<div className="workspace-settings-table-wrap mb-4">
@ -192,7 +194,10 @@ const UsersTable = ({
>
{user.status}
</small>
{user.status === 'invited' && !hideAccountSetupLink && user?.invitation_token ? (
{user.status === 'invited' &&
!hideAccountSetupLink &&
user?.invitation_token &&
edition != 'cloud' ? (
<div className="workspace-clipboard-wrap">
<CopyToClipboard text={generateInvitationURL(user)} onCopy={invitationLinkCopyHandler}>
<span>

View file

@ -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"
>
<SolidIcon name="search" width="14" fill={darkMode ? '#CFD3D8E6' : '#6A727C'} />
</Button>

View file

@ -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) {

View file

@ -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)

View file

@ -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 });
}

View file

@ -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'],

View file

@ -1,2 +0,0 @@
dist
migrations

5
server/.gitignore vendored
View file

@ -31,4 +31,7 @@ lerna-debug.log*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/extensions.json
# Postgrest configuration
**/postgrest.conf

View file

@ -1 +1 @@
14.17.3
22.15.1

View file

@ -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<void> {
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) => {

View file

@ -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<void> {
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<SSOConfigs>[] = [
{
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 || '',
},
},
{

View file

@ -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<void> {
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

View file

@ -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<void> {

View file

@ -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;
`);
}
}
}

View file

@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddAutoActivatedToUsers1738235725332 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'users',
new TableColumn({
name: 'auto_activated',
type: 'boolean',
default: false,
isNullable: false,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('users', 'auto_activated');
}
}

View file

@ -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.`;
}
}

View file

@ -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.`;
}
}

View file

@ -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;

View file

@ -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>(LicenseInitService);
const licenseUtilService = app.get<ILicenseUtilService>(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<string>('TOOLJET_HOST');
const host = new URL(tooljetHost);
const domain = host.hostname;
logger.log(`Configuring CORS for domain: ${domain}`);
logger.log(`CORS enabled: ${configService.get<string>('ENABLE_CORS') === 'true'}`);
// Enable CORS
app.enableCors({
origin: configService.get<string>('ENABLE_CORS') === 'true' || tooljetHost,
credentials: true,
});
// Get CSP whitelisted domains
const cspWhitelistedDomains = configService.get<string>('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<string>('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<string>('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<string>('TOOLJET_HOST');
const subPath = configService.get<string>('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<string>('TOOLJET_HTTP_PROXY') || 'Not configured'}`);
logger.log(`Frame embedding: ${configService.get<string>('DISABLE_APP_EMBED') !== 'true' ? 'enabled' : 'disabled'}`);
logger.log(`Environment: ${configService.get<string>('NODE_ENV') || 'development'}`);
logger.log(`Port: ${configService.get<string>('PORT') || 3000}`);
logger.log(`Listen Address: ${configService.get<string>('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...');
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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>(LicenseInitService);
const licenseUtilService = app.get<ILicenseUtilService>(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<NestExpressApplication>(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<NestExpressApplication>(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>(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>(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<string>('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<string>('TOOLJET_HOST');
const subPath = configService.get<string>('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<ITemporalService>(TemporalService);
await temporalService.runWorker();
await appContext.close();
const temporalService = appContext.get<ITemporalService>(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<ITemporalService>(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();

View file

@ -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;
}
}

View file

@ -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<typeof App> | 'all';
type Subjects = InferSubjects<typeof AppGitSync> | 'all';
export type AppGitAbility = Ability<[FEATURE_KEY, Subjects]>;
@Injectable()
export class AppGitAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects> {
export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects> {
protected getSubjectType() {
return App;
return AppGitSync;
}
protected defineAbilityFor(
@ -21,48 +22,44 @@ export class AppGitAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
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<FEATURE_KEY, Subjects>
appId &&
userAppGitPermissions.editableAppsId.includes(appId)
) {
can(FEATURE_KEY.GIT_GET_APP_CONFIG, App);
can(FEATURE_KEY.GIT_GET_APP_CONFIG, AppGitSync);
}
}
}

View file

@ -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
},
};

View file

@ -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,

View file

@ -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<boolean> {
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;
}
}

View file

@ -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],

View file

@ -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,
};

View file

@ -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',
}

View file

@ -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<boolean> {
return getTooljetEdition() === TOOLJET_EDITIONS.Cloud;
}
}

View file

@ -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<string>();
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;
}
});

View file

@ -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(

View file

@ -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,
},
},
};

View file

@ -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'
}

View file

@ -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 || {};
});

View file

@ -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;
}

View file

@ -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<any>;
@ -16,3 +17,39 @@ export interface IAuthController {
forgotPassword(appAuthDto: AppForgotPasswordDto): Promise<Record<string, never>>;
resetPassword(appAuthDto: AppPasswordResetDto): Promise<Record<string, never>>;
}
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<any>;
/**
* 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<any>;
/**
* 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<string, any>): 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<string, any>): any;
}

View file

@ -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<any>;
@ -9,3 +11,38 @@ export interface IAuthService {
forgotPassword(email: string): Promise<void>;
resetPassword(token: string, password: string): Promise<void>;
}
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<any>;
/**
* 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<string, any>): { 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<string, any>): { message: string };
}

View file

@ -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],
};

View file

@ -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);
}

View file

@ -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 {

View file

@ -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<string, any>) {
throw new NotImplementedException();
}
@InitFeature(FEATURE_KEY.AI_COOKIE_DELETE)
@Get('delete-ai-cookies')
@UseGuards(FeatureAbilityGuard)
deleteAiCookies(@Res({ passthrough: true }) response: Response, @AiCookies() cookies: Record<string, any>) {
throw new NotImplementedException();
}
}

View file

@ -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<string, any>) {
return { message: 'AI Cookies set successfully' };
}
clearSessionAICookies(response: Response, cookies: Record<string, any>) {
return { message: 'AI Cookies cleared successfully' };
}
}

View file

@ -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],

View file

@ -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);

View file

@ -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!';

View file

@ -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;
}
}

View file

@ -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<typeof OrganizationGitSync> | 'all';
export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>;
@Injectable()
export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects> {
protected getSubjectType() {
return OrganizationGitSync;
}
protected defineAbilityFor(
can: AbilityBuilder<FeatureAbility>['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);
}
}

View file

@ -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 },
},
};

View file

@ -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',
}

View file

@ -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() {}

View file

@ -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<DynamicModule> {
@ -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],
};
}
}

View file

@ -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;
}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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<Comment> {
constructor(
@InjectRepository(Comment)
private commentRepository: Repository<Comment>
) {
super(commentRepository.target, commentRepository.manager, commentRepository.queryRunner);
}
public async createComment(
createCommentDto: CreateCommentDto,
userId: string,
organizationId: string
): Promise<Comment> {
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<Comment> {
const { comment, threadId } = updateCommentDto;
editedComment.comment = comment;
editedComment.threadId = threadId;
return this.commentRepository.save(editedComment);
}
}

View file

@ -1,24 +0,0 @@
import { DataSource, EntityManager, Repository } from 'typeorm';
import { GroupPermissions } from '@entities/group_permissions.entity';
interface IGroupPermissionRepository extends Repository<GroupPermissions> {
getAllUserGroups(userId: string, organizationId: string, manager: EntityManager): Promise<GroupPermissions[]>;
}
export class GroupPermissionRepository extends Repository<GroupPermissions> implements IGroupPermissionRepository {
constructor(private dataSource: DataSource) {
super(GroupPermissions, dataSource.createEntityManager());
}
getAllUserGroups(userId: string, organizationId: string, manager: EntityManager): Promise<GroupPermissions[]> {
return manager.find(GroupPermissions, {
where: {
organizationId: organizationId,
groupUsers: {
userId: userId,
},
},
relations: ['groupUsers'],
});
}
}

View file

@ -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<SSOConfigs> {
async findByOrganizationId(organizationId: string): Promise<SSOConfigs[]> {
return this.find({ where: { organizationId } });
}
async findInstanceConfigs(): Promise<SSOConfigs[]> {
return this.find({ where: { organizationId: null } });
}
async createOrUpdateSSOConfig(configData: Partial<SSOConfigs>): Promise<SSOConfigs> {
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<SSOConfigs>): Promise<SSOConfigs> {
await this.update(id, updateData);
return this.findOne({ where: { id } });
}
async deleteConfig(id: string): Promise<void> {
await this.delete(id);
}
async getSSOConfigsForOrganization(organizationId: string, sso: SSOType | string): Promise<SSOConfigs | null> {
return this.findOne({
where: {
organizationId,
sso: sso as SSOType,
},
relations: ['organization'],
});
}
}

View file

@ -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<Thread> {
constructor(
@InjectRepository(Thread)
private threadRepository: Repository<Thread>
) {
super(threadRepository.target, threadRepository.manager, threadRepository.queryRunner);
}
public async createThread(createThreadDto: CreateThreadDto, userId: string, organizationId: string): Promise<Thread> {
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<Thread> {
const { x, y, isResolved } = updateThreadDto;
editedThread.x = x;
editedThread.y = y;
editedThread.isResolved = isResolved;
return await this.threadRepository.save(editedThread);
}
}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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<Record<TooljetDbActions | 'default', string>>;
type ErrorCodeMapping = {
[key in PostgresErrorCode]: ErrorCodeMappingItem;
};
const errorCodeMapping: Partial<ErrorCodeMapping> = {
[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, string>): 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<string, string> | 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;
}
}

View file

@ -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'");
}
}
}

Some files were not shown because too many files have changed in this diff Show more