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 }) => { >
Page permission
- {!licenseValid && } + {!licenseValid && }
); diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx index a113251b61..6c74e7b6af 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx @@ -17,6 +17,7 @@ import IconSelector from './IconSelector'; import { withRouter } from '@/_hoc/withRouter'; import OverflowTooltip from '@/_components/OverflowTooltip'; import { shallow } from 'zustand/shallow'; +import { ToolTip } from '@/_components/ToolTip'; export const PageMenuItem = withRouter( memo(({ darkMode, page, navigate }) => { @@ -151,6 +152,36 @@ export const PageMenuItem = withRouter( [popoverRef.current, page] ); + function getTooltip() { + const permission = page?.permissions?.length ? page?.permissions[0] : null; + if (!permission) return ''; + const users = permission.users || []; + const isSingle = permission.type === 'SINGLE'; + const isGroup = permission.type === 'GROUP'; + + if (users.length === 0) return null; + + if (isSingle) { + if (users.length === 1) { + const email = users[0].user.email; + return `Access restricted to ${email}`; + } else { + return `Access restricted to ${users.length} users`; + } + } + + if (isGroup) { + if (users.length === 1) { + const groupName = users[0].permissionGroup?.name ?? 'Group'; + return `Access restricted to ${groupName} group`; + } else { + return `Access restricted to ${users.length} groups`; + } + } + + return ''; + } + return (
setIsHovered(true)} @@ -200,7 +231,13 @@ export const PageMenuItem = withRouter(
- {licenseValid && restricted && } + {licenseValid && restricted && ( + +
+ +
+
+ )}
{!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 && ( + + )} +
+ +

+ 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} - -
- {headerAction && headerAction()} - -
-
+ {showHeader && ( + + + {title} + +
+ {headerAction && headerAction()} + +
+
+ )} {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],