diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 0e04b4b2a3..0e40e0078c 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -116,6 +116,8 @@ class HomePageComponent extends React.Component { shouldAutoImportPlugin: false, dependentPlugins: [], dependentPluginsDetail: {}, + showMissingGroupsModal: false, + missingGroups: [], }; } @@ -356,17 +358,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 +397,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 +901,8 @@ class HomePageComponent extends React.Component { showGroupMigrationBanner, dependentPlugins, dependentPluginsDetail, + showMissingGroupsModal, + missingGroups, } = this.state; const modalConfigs = { create: { @@ -953,6 +968,29 @@ 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: '', + }} + darkMode={this.props.darkMode} + > + + The following group permissions are missing for page permissions. Are you sure you want to continue? + + + {missingGroups.map((item, index) => ( + + {`${index + 1}. ${item}`} + + ))} + + {showRenameAppModal && ( this.setState({ showRenameAppModal: true })} 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/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/app-permissions/repositories/page-permissions.repository.ts b/server/src/modules/app-permissions/repositories/page-permissions.repository.ts index a36be08bc7..7caa5f4318 100644 --- a/server/src/modules/app-permissions/repositories/page-permissions.repository.ts +++ b/server/src/modules/app-permissions/repositories/page-permissions.repository.ts @@ -19,10 +19,14 @@ export class PagePermissionsRepository extends Repository { }); return pagePermissions.map((permission) => { - return { - ...permission, - users: permission.users, - }; + if (permission.type === PAGE_PERMISSION_TYPE.GROUP) { + return { + ...permission, + groups: permission.users, + users: undefined, + }; + } + return permission; }); }, manager || this.manager); } 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 6cd6b3ce8f..5b1d09f4c8 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'; interface AppResourceMappings { defaultDataSourceIdMapping: Record; dataQueryMapping: Record; @@ -51,7 +56,17 @@ type DefaultDataSourceName = | 'tooljetdbdefault' | 'workflowsdefault'; -type NewRevampedComponent = 'Text' | 'TextInput' | 'PasswordInput' | 'NumberInput' | 'Table' | 'Button' | 'Checkbox' | 'Divider' | 'VerticalDivider' | 'Link'; +type NewRevampedComponent = + | 'Text' + | 'TextInput' + | 'PasswordInput' + | 'NumberInput' + | 'Table' + | 'Button' + | 'Checkbox' + | 'Divider' + | 'VerticalDivider' + | 'Link'; const DefaultDataSourceNames: DefaultDataSourceName[] = [ 'restapidefault', @@ -82,7 +97,7 @@ export class AppImportExportService { protected appEnvironmentUtilService: AppEnvironmentUtilService, protected readonly entityManager: EntityManager, protected componentsService: ComponentsService - ) { } + ) {} async export(user: User, id: string, searchParams: any = {}): Promise<{ appV2: App }> { // https://github.com/typeorm/typeorm/issues/3857 @@ -174,23 +189,41 @@ 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 - .createQueryBuilder(Component, 'components') - .leftJoinAndSelect('components.layouts', 'layouts') - .where('components.pageId IN(:...pageId)', { - pageId: pages.map((v) => v.id), - }) - .orderBy('components.created_at', 'ASC') - .getMany() + .createQueryBuilder(Component, 'components') + .leftJoinAndSelect('components.layouts', 'layouts') + .where('components.pageId IN(:...pageId)', { + pageId: pages.map((v) => v.id), + }) + .orderBy('components.created_at', 'ASC') + .getMany() : []; const events = await manager @@ -202,7 +235,7 @@ export class AppImportExportService { .getMany(); appToExport['components'] = components; - appToExport['pages'] = pages; + appToExport['pages'] = pagesWithPermissionGroups; appToExport['events'] = events; appToExport['dataQueries'] = dataQueries; appToExport['dataSources'] = dataSources; @@ -800,6 +833,10 @@ export class AppImportExportService { }); } + if (page.permissions) { + pageCreated.permissions = page.permissions; + } + appResourceMappings.pagesMapping[page.id] = pageCreated.id; isHomePage = importingAppVersion.homePageId === page.id; @@ -808,6 +845,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 = {}; @@ -924,6 +964,7 @@ export class AppImportExportService { }); } } + // relink page groups const updateArr = []; for (const { pageId, groupId } of pageGroupIdArr) { @@ -1059,10 +1100,10 @@ export class AppImportExportService { const options = importingDataSource.kind === 'tooljetdb' ? this.replaceTooljetDbTableIds( - importingQuery.options, - externalResourceMappings['tooljet_database'], - organizationId - ) + importingQuery.options, + externalResourceMappings['tooljet_database'], + organizationId + ) : importingQuery.options; const newQuery = manager.create(DataQuery, { @@ -1315,6 +1356,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, @@ -1627,10 +1738,10 @@ export class AppImportExportService { options: dataSourceId == defaultDataSourceIds['tooljetdb'] ? this.replaceTooljetDbTableIds( - query.options, - externalResourceMappings['tooljet_database'], - user.organizationId - ) + query.options, + externalResourceMappings['tooljet_database'], + user.organizationId + ) : query.options, }); await manager.save(newQuery); 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);
+ The following group permissions are missing for page permissions. Are you sure you want to continue? +