diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx
index 1c5c3124f0..497b697b25 100644
--- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx
+++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx
@@ -164,7 +164,7 @@ export const PageHandlerMenu = ({ darkMode }) => {
>
setIsHovered(true)}
@@ -200,7 +231,13 @@ export const PageMenuItem = withRouter(
{!shouldFreeze && (
diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx
index 6a4a1c516a..e82f251665 100644
--- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx
+++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx
@@ -34,7 +34,6 @@ export default function PagePermission({ darkMode }) {
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isPermissionsLoading, setPermissionsLoading] = useState(true);
- const [pageToDelete, setPageToDelete] = useState(null);
const [initialSelectedGroups, setInitialSelectedGroups] = useState([]);
const [initialSelectedUsers, setInitialSelectedUsers] = useState([]);
const [initalPagePermissionType, setInitialPagePermissionType] = useState('all');
@@ -42,7 +41,7 @@ export default function PagePermission({ darkMode }) {
useEffect(() => {
if (!showPagePermissionModal) return;
const fetchPagePermission = () => {
- appPermissionService.getPagePermission(appId, editingPage?.id || pageToDelete).then((data) => {
+ appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => {
if (data) {
if (data[0] && data[0]?.type === PERMISSION_TYPES.group) {
const groups =
@@ -55,7 +54,6 @@ export default function PagePermission({ darkMode }) {
setInitialPagePermissionType(data[0]?.type?.toLowerCase());
setPagePermission(data);
toggleUserGroupSelect(true);
- setPageToDelete(null);
setInitialSelectedGroups(groups);
data?.length && setSelectedUserGroups(groups);
} else if (data[0] && data[0]?.type === PERMISSION_TYPES.single) {
@@ -74,7 +72,6 @@ export default function PagePermission({ darkMode }) {
setInitialPagePermissionType(data[0]?.type?.toLowerCase());
setPagePermission(data);
toggleUsersSelect(true);
- setPageToDelete(null);
setInitialSelectedUsers(users);
data?.length && setSelectedUsers(users);
}
@@ -83,7 +80,7 @@ export default function PagePermission({ darkMode }) {
});
};
fetchPagePermission();
- }, [showPagePermissionModal, pageToDelete]);
+ }, [showPagePermissionModal]);
const isSelectionUnchanged = useMemo(() => {
if (pagePermissionType === 'group') {
@@ -237,13 +234,12 @@ export default function PagePermission({ darkMode }) {
const deletePagePermission = () => {
setIsLoading(true);
appPermissionService
- .deletePagePermission(appId, pageToDelete)
+ .deletePagePermission(appId, editingPage?.id)
.then((data) => {
toast.success('Permission successfully deleted!', {
className: 'text-nowrap w-auto mw-100',
});
- updatePageWithPermissions(pageToDelete, []);
- setPageToDelete(null);
+ updatePageWithPermissions(editingPage?.id, []);
})
.catch(() => {
toast.error('Permission could not be deleted. Please try again!', {
@@ -284,25 +280,18 @@ export default function PagePermission({ darkMode }) {
isLoading={isLoading}
handleClose={handlePagePermissionModalClose}
confirmBtnProps={{
- title: pagePermission ? 'Update' : pagePermissionType === 'all' ? 'Default permission' : 'Create permission',
+ title: pagePermission
+ ? 'Save changes'
+ : pagePermissionType === 'all'
+ ? 'Default permission'
+ : 'Create permission',
disabled: isPermissionsLoading || isSelectionUnchanged,
tooltipMessage: '',
+ leftIcon: pagePermission && 'save',
+ className: 'action-btn-page-permission',
}}
darkMode={darkMode}
className="page-permissions-modal"
- headerAction={() =>
- pagePermission && (
-
{
- setPageToDelete(editingPage?.id);
- togglePagePermissionModal(false);
- setShowConfirmDelete(true);
- }}
- >
-
-
- )
- }
>
{isPermissionsLoading ? (
diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss
index 968218b106..a3adc9ec20 100644
--- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss
+++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss
@@ -374,4 +374,8 @@
.spinner-center {
min-height: 250px;
}
+}
+
+.modal-base .modal-footer .action-btn-page-permission svg path {
+ fill: var(--indigo1) !important;
}
\ No newline at end of file
diff --git a/frontend/src/AppBuilder/Viewer/PageGroup.jsx b/frontend/src/AppBuilder/Viewer/PageGroup.jsx
index 0311115b09..1739263fff 100644
--- a/frontend/src/AppBuilder/Viewer/PageGroup.jsx
+++ b/frontend/src/AppBuilder/Viewer/PageGroup.jsx
@@ -11,8 +11,6 @@ import cx from 'classnames';
const RenderPage = ({ page, currentPageId, switchPageWrapper, labelStyle, computeStyles, darkMode, homePageId }) => {
const isHomePage = page.id === homePageId;
- console.log({ page, homePageId });
- console.log({ isHomePage });
const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon;
const IconElement = Icons?.[iconName] ?? Icons?.['IconFileDescription'];
return (page.hidden || page.disabled) && page?.restricted ? null : (
diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx
index 0e04b4b2a3..3af41903aa 100644
--- a/frontend/src/HomePage/HomePage.jsx
+++ b/frontend/src/HomePage/HomePage.jsx
@@ -47,6 +47,7 @@ import {
ConsultationBanner,
} from '@/modules/dashboard/components';
import CreateAppWithPrompt from '@/modules/AiBuilder/components/CreateAppWithPrompt';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
const { iconList, defaultIcon } = configs;
@@ -116,6 +117,9 @@ class HomePageComponent extends React.Component {
shouldAutoImportPlugin: false,
dependentPlugins: [],
dependentPluginsDetail: {},
+ showMissingGroupsModal: false,
+ missingGroups: [],
+ missingGroupsExpanded: false,
};
}
@@ -356,17 +360,24 @@ class HomePageComponent extends React.Component {
}
};
- importFile = async (importJSON, appName) => {
+ importFile = async (importJSON, appName, skipPagePermissionsGroupCheck = false) => {
this.setState({ isImportingApp: true });
// For backward compatibility with legacy app import
const organization_id = this.state.currentUser?.organization_id;
const isLegacyImport = isEmpty(importJSON.tooljet_version);
if (isLegacyImport) {
- importJSON = { app: [{ definition: importJSON, appName: appName }], tooljet_version: importJSON.tooljetVersion };
+ importJSON = {
+ app: [{ definition: importJSON, appName: appName }],
+ tooljet_version: importJSON.tooljetVersion,
+ };
} else {
importJSON.app[0].appName = appName;
}
- const requestBody = { organization_id, ...importJSON };
+ const requestBody = {
+ organization_id,
+ ...importJSON,
+ skip_page_permissions_group_check: skipPagePermissionsGroupCheck,
+ };
let installedPluginsInfo = [];
try {
if (this.state.dependentPlugins.length) {
@@ -388,6 +399,10 @@ class HomePageComponent extends React.Component {
this.props.navigate(`/${getWorkspaceId()}/database`);
}
} catch (error) {
+ if (error?.error?.type === 'permission-check') {
+ this.setState({ showMissingGroupsModal: true, missingGroups: error?.error?.data });
+ return;
+ }
if (installedPluginsInfo.length) {
const pluginsId = installedPluginsInfo.map((pluginInfo) => pluginInfo.id);
await pluginsService.uninstallPlugins(pluginsId);
@@ -888,6 +903,9 @@ class HomePageComponent extends React.Component {
showGroupMigrationBanner,
dependentPlugins,
dependentPluginsDetail,
+ showMissingGroupsModal,
+ missingGroups,
+ missingGroupsExpanded,
} = this.state;
const modalConfigs = {
create: {
@@ -939,6 +957,12 @@ class HomePageComponent extends React.Component {
};
const isAdmin = authenticationService?.currentSessionValue?.admin;
const isBuilder = authenticationService?.currentSessionValue?.is_builder;
+
+ //import app missing groups modal config
+ const threshold = 3;
+ const isLong = missingGroups.length > threshold;
+ const displayedGroups = missingGroupsExpanded ? missingGroups : missingGroups.slice(0, threshold);
+
return (
@@ -953,6 +977,93 @@ class HomePageComponent extends React.Component {
configs={modalConfigs}
onCommitChange={this.handleCommitChange}
/>
+
this.importFile(fileContent, fileName, true)}
+ show={showMissingGroupsModal}
+ isLoading={importingApp}
+ handleClose={() => this.setState({ showMissingGroupsModal: false })}
+ confirmBtnProps={{
+ title: 'Import',
+ tooltipMessage: '',
+ }}
+ className="missing-groups-modal"
+ darkMode={this.props.darkMode}
+ >
+
+
+
+
+
Warning: Missing user groups for permissions
+
+ Permissions for the following user group(s) won’t be applied since they do not exist in this
+ workspace.
+
+
+
+
+
+
+
+ User groups
+
+
+ {displayedGroups.map((group, idx) => (
+
+ {group}
+ {idx < displayedGroups.length - 1 ? ', ' : ''}
+
+ ))}
+ {!missingGroupsExpanded && isLong && '...'}
+
+
+
+ {isLong && (
+
this.setState({ missingGroupsExpanded: !missingGroupsExpanded })}
+ >
+
+
+
+ {missingGroupsExpanded ? 'See less' : 'See more'}
+
+ )}
+
+
+
+ Restricted pages, queries, or components will become accessible to all users or to existing groups with
+ permissions. To avoid this, create the missing groups before importing, or reconfigure permissions after
+ import.
+
+
+
+ this.setState({ showMissingGroupsModal: false, isImportingApp: false })}
+ >
+ Cancel import
+
+ this.importFile(fileContent, fileName, true)}
+ className="primary-action"
+ >
+ Import with limited permissions
+
+
+
+
{showRenameAppModal && (
this.setState({ showRenameAppModal: true })}
diff --git a/frontend/src/_styles/components.scss b/frontend/src/_styles/components.scss
index 074338602e..059841e6c7 100644
--- a/frontend/src/_styles/components.scss
+++ b/frontend/src/_styles/components.scss
@@ -242,10 +242,16 @@ $btn-dark-color: #FFFFFF;
display: flex;
align-items: baseline;
gap: 5px;
+ cursor: pointer !important;
+ pointer-events: unset !important;
&.disabled {
opacity: 1 !important;
}
+
+ svg {
+ margin-left: 5px;
+ }
}
}
diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss
index f79ec46931..da4447da2a 100644
--- a/frontend/src/_styles/theme.scss
+++ b/frontend/src/_styles/theme.scss
@@ -18891,4 +18891,69 @@ section.ai-message-prompt-input-wrapper {
.cm-editor {
max-height: 100px !important;
}
+}
+
+.missing-groups-modal {
+ .modal-body {
+ padding: 16px;
+
+ .header {
+ padding-top: 12px;
+ font-weight: 500;
+ font-size: 14px;
+ }
+
+ .sub-header {
+ margin-bottom: 0px;
+ font-size: 12px;
+ }
+
+ .groups-list {
+ padding-top: 16px;
+ padding-bottom: 16px;
+
+ .container {
+ padding: 12px;
+ }
+ }
+
+ .info {
+ margin-bottom: 0px;
+ font-size: 12px;
+ padding-bottom: 24px;
+ }
+
+ .action-btns {
+ justify-content: space-between;
+ }
+
+ .primary-action, .secondary-action {
+ padding: 8px !important;
+ font-size: 12px;
+ }
+
+ .toggle-button {
+ display: inline-flex;
+ align-items: center;
+ font-size: 14px;
+ color: var(--icon-brand);
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ font-family: inherit;
+ }
+
+ .toggle-button:hover {
+ text-decoration: underline;
+ }
+
+ .toggle-button .chevron {
+ transition: transform 0.2s ease;
+ }
+
+ .toggle-button.expanded .chevron {
+ transform: rotate(180deg);
+ }
+ }
}
\ No newline at end of file
diff --git a/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx b/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx
new file mode 100644
index 0000000000..eed8dd5e8c
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+const EnterpriseCrown = ({ fill = '#FCA23F', width = '12', className = '', viewBox = '0 0 16 16' }) => (
+
+
+
+);
+
+export default EnterpriseCrown;
diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js
index 2a7b801570..11ab593839 100644
--- a/frontend/src/_ui/Icon/solidIcons/index.js
+++ b/frontend/src/_ui/Icon/solidIcons/index.js
@@ -234,6 +234,7 @@ import NewTabSmall from './NewTabSmall.jsx';
import Code from './Code.jsx';
import WorkflowV3 from './WorkflowV3.jsx';
import WorkspaceV3 from './WorkspaceV3.jsx';
+import EnterpriseCrown from './EnterrpiseCrown.jsx';
import Moon from './Moon.jsx';
const Icon = (props) => {
@@ -356,6 +357,8 @@ const Icon = (props) => {
return ;
case 'enterprisev3':
return ;
+ case 'enterprisecrown':
+ return ;
case 'lockGradient':
return ;
case 'datasourceGradient':
diff --git a/frontend/src/_ui/Modal/index.jsx b/frontend/src/_ui/Modal/index.jsx
index b9d48f0d88..6ed7fa04db 100644
--- a/frontend/src/_ui/Modal/index.jsx
+++ b/frontend/src/_ui/Modal/index.jsx
@@ -18,6 +18,8 @@ export default function ModalBase({
className = '',
size = 'sm',
headerAction,
+ showHeader = true,
+ showFooter = true,
}) {
return (
-
-
- {title}
-
-
-
+ {showHeader && (
+
+
+ {title}
+
+
+
+ )}
{children ? (
children
@@ -45,28 +49,30 @@ export default function ModalBase({
)}
-
-
- Cancel
-
-
-
-
- {confirmBtnProps?.title || 'Continue'}
-
-
-
-
+ {showFooter && (
+
+
+ Cancel
+
+
+
+
+ {confirmBtnProps?.title || 'Continue'}
+
+
+
+
+ )}
);
}
diff --git a/server/migrations/1744610362161-CreatePagePermissions.ts b/server/migrations/1744610362161-CreatePagePermissions.ts
index ca4afbac66..ebf622da8b 100644
--- a/server/migrations/1744610362161-CreatePagePermissions.ts
+++ b/server/migrations/1744610362161-CreatePagePermissions.ts
@@ -1,13 +1,7 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
-import { TOOLJET_EDITIONS } from '@modules/app/constants';
-import { getTooljetEdition } from '@helpers/utils.helper';
export class CreatePagePermissions1744610362161 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise {
- if (getTooljetEdition() === TOOLJET_EDITIONS.CE) {
- return;
- }
-
await queryRunner.createTable(
new Table({
name: 'page_permissions',
diff --git a/server/migrations/1744611380594-CreatePageUsers.ts b/server/migrations/1744611380594-CreatePageUsers.ts
index 5fe4d126c7..f1c6c89beb 100644
--- a/server/migrations/1744611380594-CreatePageUsers.ts
+++ b/server/migrations/1744611380594-CreatePageUsers.ts
@@ -1,13 +1,7 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
-import { TOOLJET_EDITIONS } from '@modules/app/constants';
-import { getTooljetEdition } from '@helpers/utils.helper';
export class CreatePageUsers1744611380594 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise {
- if (getTooljetEdition() === TOOLJET_EDITIONS.CE) {
- return;
- }
-
await queryRunner.createTable(
new Table({
name: 'page_users',
diff --git a/server/src/dto/import-resources.dto.ts b/server/src/dto/import-resources.dto.ts
index f00992213e..89b3ee182c 100644
--- a/server/src/dto/import-resources.dto.ts
+++ b/server/src/dto/import-resources.dto.ts
@@ -1,4 +1,4 @@
-import { IsUUID, IsOptional, IsString, IsDefined, ValidateNested } from 'class-validator';
+import { IsUUID, IsOptional, IsString, IsDefined, ValidateNested, IsBoolean } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { ValidateTooljetDatabaseSchema } from './validators/tooljet-database.validator';
import { TjdbSchemaToLatestVersion } from './transformers/resource-transformer';
@@ -28,6 +28,10 @@ export class ImportResourcesDto {
// and instantiated data
@ValidateTooljetDatabaseSchema({ each: true })
tooljet_database: ImportTooljetDatabaseDto[];
+
+ @IsOptional()
+ @IsBoolean()
+ skip_page_permissions_group_check?: boolean;
}
export class ImportAppDto {
diff --git a/server/src/entities/group_permissions.entity.ts b/server/src/entities/group_permissions.entity.ts
index 92868d7510..693f4f930c 100644
--- a/server/src/entities/group_permissions.entity.ts
+++ b/server/src/entities/group_permissions.entity.ts
@@ -3,7 +3,6 @@ import {
Column,
CreateDateColumn,
Entity,
- Index,
JoinColumn,
ManyToOne,
OneToMany,
@@ -21,7 +20,6 @@ export class GroupPermissions extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
- @Index()
@Column({ name: 'organization_id', nullable: false })
organizationId: string;
diff --git a/server/src/entities/group_users.entity.ts b/server/src/entities/group_users.entity.ts
index 29771a5557..03ac55386b 100644
--- a/server/src/entities/group_users.entity.ts
+++ b/server/src/entities/group_users.entity.ts
@@ -3,7 +3,6 @@ import {
Column,
CreateDateColumn,
Entity,
- Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
@@ -17,11 +16,9 @@ export class GroupUsers extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
- @Index()
@Column({ name: 'user_id', nullable: false })
userId: string;
- @Index()
@Column({ name: 'group_id', nullable: false })
groupId: string;
diff --git a/server/src/entities/page_users.entity.ts b/server/src/entities/page_users.entity.ts
index ca3ef77c65..960be5b32f 100644
--- a/server/src/entities/page_users.entity.ts
+++ b/server/src/entities/page_users.entity.ts
@@ -1,4 +1,4 @@
-import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, Index } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm';
import { User } from './user.entity';
import { PagePermission } from './page_permissions.entity';
import { GroupPermissions } from './group_permissions.entity';
@@ -8,15 +8,12 @@ export class PageUser {
@PrimaryGeneratedColumn('uuid')
id: string;
- @Index()
@Column({ name: 'page_permissions_id', type: 'uuid' })
pagePermissionsId: string;
- @Index()
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string | null;
- @Index()
@Column({ name: 'permission_groups_id', type: 'uuid', nullable: true })
permissionGroupsId: string | null;
diff --git a/server/src/helpers/error_type.constant.ts b/server/src/helpers/error_type.constant.ts
index 4b968f002e..cb483e8896 100644
--- a/server/src/helpers/error_type.constant.ts
+++ b/server/src/helpers/error_type.constant.ts
@@ -1,5 +1,7 @@
export const APP_ERROR_TYPE = {
IMPORT_EXPORT_SERVICE: {
UNSUPPORTED_VERSION_ERROR: 'Apps built on later versions of ToolJet cannot be imported',
+ PAGE_PERMISSION_GROUP_ERROR: 'Following groups are missing from the workspace',
+ PERMISSION_CHECK: 'permission-check',
},
};
diff --git a/server/src/modules/apps/module.ts b/server/src/modules/apps/module.ts
index 6565c17ed1..2a54c29326 100644
--- a/server/src/modules/apps/module.ts
+++ b/server/src/modules/apps/module.ts
@@ -39,15 +39,7 @@ export class AppsModule {
return {
module: AppsModule,
imports: [
- TypeOrmModule.forFeature([
- App,
- Page,
- EventHandler,
- Organization,
- Component,
- VersionRepository,
- RolesRepository,
- ]),
+ TypeOrmModule.forFeature([App, Page, EventHandler, Organization, Component, VersionRepository]),
await FolderAppsModule.register(configs),
await ThemesModule.register(configs),
await FoldersModule.register(configs),
diff --git a/server/src/modules/apps/services/app-import-export.service.ts b/server/src/modules/apps/services/app-import-export.service.ts
index 16fa8289f6..5d6da2ea7d 100644
--- a/server/src/modules/apps/services/app-import-export.service.ts
+++ b/server/src/modules/apps/services/app-import-export.service.ts
@@ -1,4 +1,4 @@
-import { BadRequestException, Injectable } from '@nestjs/common';
+import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { isEmpty, set } from 'lodash';
import { App } from 'src/entities/app.entity';
import { AppEnvironment } from 'src/entities/app_environments.entity';
@@ -33,6 +33,11 @@ import { DataSourcesUtilService } from '@modules/data-sources/util.service';
import { DataSourcesRepository } from '@modules/data-sources/repository';
import { AppEnvironmentUtilService } from '@modules/app-environments/util.service';
import { ComponentsService } from './component.service';
+import { GroupPermissions } from '@entities/group_permissions.entity';
+import { APP_ERROR_TYPE } from '@helpers/error_type.constant';
+import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants';
+import { PagePermission } from '@entities/page_permissions.entity';
+import { PageUser } from '@entities/page_users.entity';
import { UsersUtilService } from '@modules/users/util.service';
interface AppResourceMappings {
defaultDataSourceIdMapping: Record;
@@ -186,13 +191,31 @@ export class AppImportExportService {
}
const pages = await manager
- .createQueryBuilder(Page, 'pages')
- .where('pages.appVersionId IN(:...versionId)', {
+ .createQueryBuilder(Page, 'page')
+ .leftJoinAndSelect('page.permissions', 'permission')
+ .leftJoinAndSelect('permission.users', 'pageUser')
+ .leftJoinAndSelect('pageUser.permissionGroup', 'permissionGroup')
+ .where('page.appVersionId IN(:...versionId)', {
versionId: appVersions.map((v) => v.id),
})
- .orderBy('pages.created_at', 'ASC')
+ .orderBy('page.created_at', 'ASC')
.getMany();
+ const pagesWithPermissionGroups = pages.map((page) => {
+ const groupPermission = page.permissions.find((perm) => perm.type === 'GROUP');
+
+ return {
+ ...page,
+ permissions: groupPermission
+ ? {
+ permissionGroup: groupPermission.users
+ .map((user) => user.permissionGroup?.name)
+ .filter((name): name is string => Boolean(name)),
+ }
+ : undefined,
+ };
+ });
+
const components =
pages.length > 0
? await manager
@@ -214,7 +237,7 @@ export class AppImportExportService {
.getMany();
appToExport['components'] = components;
- appToExport['pages'] = pages;
+ appToExport['pages'] = pagesWithPermissionGroups;
appToExport['events'] = events;
appToExport['dataQueries'] = dataQueries;
appToExport['dataSources'] = dataSources;
@@ -812,6 +835,10 @@ export class AppImportExportService {
});
}
+ if (page.permissions) {
+ pageCreated.permissions = page.permissions;
+ }
+
appResourceMappings.pagesMapping[page.id] = pageCreated.id;
isHomePage = importingAppVersion.homePageId === page.id;
@@ -820,6 +847,9 @@ export class AppImportExportService {
updateHomepageId = pageCreated.id;
}
+ //create page permissions of page if flag enabled in dto
+ await this.createPagePermissionsForGroups(pageCreated, user.organizationId, manager);
+
const pageComponents = importingComponents.filter((component) => component.pageId === page.id);
const newComponentIdsMap = {};
@@ -936,6 +966,7 @@ export class AppImportExportService {
});
}
}
+
// relink page groups
const updateArr = [];
for (const { pageId, groupId } of pageGroupIdArr) {
@@ -1327,6 +1358,76 @@ export class AppImportExportService {
return pageSettings;
}
+ async checkIfGroupPermissionsExist(pages, organizationId) {
+ const allGroupNames = new Set();
+
+ for (const page of pages) {
+ const groupNames = page.permissions?.permissionGroup || [];
+ for (const name of groupNames) {
+ allGroupNames.add(name);
+ }
+ }
+
+ if (!allGroupNames.size) return;
+
+ return await dbTransactionWrap(async (manager: EntityManager) => {
+ const existingGroups = await manager
+ .createQueryBuilder(GroupPermissions, 'gp')
+ .where('gp.name IN (:...names)', { names: Array.from(allGroupNames) })
+ .andWhere('gp.organizationId = :organizationId', { organizationId })
+ .select(['gp.name'])
+ .getMany();
+
+ const existingGroupNames = new Set(existingGroups.map((g) => g.name));
+
+ const missingGroups = Array.from(allGroupNames).filter((name) => !existingGroupNames.has(name));
+
+ if (missingGroups.length > 0) {
+ throw new HttpException(
+ {
+ message: { type: APP_ERROR_TYPE.IMPORT_EXPORT_SERVICE.PERMISSION_CHECK, data: missingGroups },
+ },
+ HttpStatus.BAD_REQUEST
+ );
+ }
+ });
+ }
+
+ async createPagePermissionsForGroups(page, organizationId: string, manager: EntityManager) {
+ const groupNames = page.permissions?.permissionGroup || [];
+ if (!groupNames.length) return;
+
+ const existingGroups = await manager
+ .createQueryBuilder(GroupPermissions, 'gp')
+ .where('gp.name IN (:...names)', { names: groupNames })
+ .andWhere('gp.organizationId = :organizationId', { organizationId })
+ .getMany();
+
+ const groupMap = new Map(existingGroups.map((g) => [g.name, g]));
+
+ // Filter to only existing group names
+ const validGroupNames = groupNames.filter((name) => groupMap.has(name));
+
+ // If no valid group names exist, do not create permissions
+ if (!validGroupNames.length) return;
+
+ const permission = manager.create(PagePermission, {
+ pageId: page.id,
+ type: PAGE_PERMISSION_TYPE.GROUP,
+ });
+
+ const savedPermission = await manager.save(permission);
+
+ const pageUsers = validGroupNames.map((name) =>
+ manager.create(PageUser, {
+ pagePermissionsId: savedPermission.id,
+ permissionGroupsId: groupMap.get(name).id,
+ })
+ );
+
+ await manager.save(pageUsers);
+ }
+
async createAppVersionsForImportedApp(
manager: EntityManager,
user: User,
diff --git a/server/src/modules/import-export-resources/service.ts b/server/src/modules/import-export-resources/service.ts
index e8292c8fd7..e47205258e 100644
--- a/server/src/modules/import-export-resources/service.ts
+++ b/server/src/modules/import-export-resources/service.ts
@@ -69,6 +69,18 @@ export class ImportExportResourcesService {
let tableNameMapping = {};
const imports = { app: [], tooljet_database: [], tableNameMapping: {} };
const importingVersion = importResourcesDto.tooljet_version;
+ const skipPagePermissionsGroupCheck = importResourcesDto.skip_page_permissions_group_check;
+
+ if (!isEmpty(importResourcesDto.app) && !skipPagePermissionsGroupCheck) {
+ for (const appImportDto of importResourcesDto.app) {
+ let appParams = appImportDto.definition;
+ if (appParams?.appV2) {
+ appParams = { ...appParams.appV2 };
+ const pages = appParams?.pages;
+ pages?.length && (await this.appImportExportService.checkIfGroupPermissionsExist(pages, user.organizationId));
+ }
+ }
+ }
if (!isEmpty(importResourcesDto.tooljet_database)) {
const res = await this.tooljetDbImportExportService.bulkImport(importResourcesDto, importingVersion, cloning);
diff --git a/server/src/modules/versions/module.ts b/server/src/modules/versions/module.ts
index 261401a18d..7fac84f033 100644
--- a/server/src/modules/versions/module.ts
+++ b/server/src/modules/versions/module.ts
@@ -9,6 +9,7 @@ import { DataSourcesModule } from '@modules/data-sources/module';
import { AppsRepository } from '@modules/apps/repository';
import { FeatureAbilityFactory } from './ability';
import { getImportPath } from '@modules/app/constants';
+import { AppPermissionsModule } from '@modules/app-permissions/module';
export class VersionModule {
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise {
@@ -33,6 +34,7 @@ export class VersionModule {
await DataSourcesModule.register(configs),
await AppEnvironmentsModule.register(configs),
await ThemesModule.register(configs),
+ await AppPermissionsModule.register(configs),
],
controllers: [ComponentsController, EventsController, PagesController, VersionController, VersionControllerV2],
providers: [
diff --git a/server/src/modules/workflows/module.ts b/server/src/modules/workflows/module.ts
index 77dfbd0af3..389a754701 100644
--- a/server/src/modules/workflows/module.ts
+++ b/server/src/modules/workflows/module.ts
@@ -71,7 +71,6 @@ export class WorkflowsModule {
WorkflowExecutionNode,
WorkflowExecutionNode,
WorkflowExecutionEdge,
- RolesRepository,
]),
ThrottlerModule.forRootAsync({
imports: [ConfigModule],