diff --git a/frontend/ee b/frontend/ee index 387bb6e55b..c5513e3034 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 387bb6e55bc6a7600b7125bb9e22ca2b17dfe65d +Subproject commit c5513e303482c45289a974bb1c918ae22be6ec9c diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx index d4b8573c7d..68a54c2ca4 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx @@ -12,11 +12,12 @@ import './style.scss'; import { SortableTree } from './Tree/SortableTree'; import { PageGroupMenu } from './AddPageButton'; import { PageHandlerMenu } from './PageHandlerMenu.jsx'; +import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal'; import { EditModal } from './EditModal'; import { SettingsModal } from './SettingsModal'; import { DeletePageConfirmationModal } from './DeletePageConfirmationModal'; import SolidIcon from '@/_ui/Icon/SolidIcons'; -import PagePermission from './PagePermission'; +import { appPermissionService } from '@/_services'; export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { const showAddNewPageInput = useStore((state) => state.showAddNewPageInput); @@ -27,6 +28,12 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { const shouldFreeze = useStore((state) => state.getShouldFreeze()); const enableReleasedVersionPopupState = useStore((state) => state.enableReleasedVersionPopupState); const closePageEditPopover = useStore((state) => state.closePageEditPopover); + const editingPageId = useStore((state) => state.editingPage?.id); + const editingPageName = useStore((state) => state.editingPage?.name); + const showPagePermissionModal = useStore((state) => state.showPagePermissionModal); + const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); + const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions); + useEffect(() => { return () => { closePageEditPopover(); @@ -95,7 +102,21 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { >
- {isLicensed ? : <>} + {isLicensed && ( + appPermissionService.getPagePermission(appId, id)} + createPermission={(id, appId, body) => appPermissionService.createPagePermission(appId, id, body)} + updatePermission={(id, appId, body) => appPermissionService.updatePagePermission(appId, id, body)} + deletePermission={(id, appId) => appPermissionService.deletePagePermission(appId, id)} + onSuccess={(data) => updatePageWithPermissions(editingPageId, data)} + /> + )} diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx deleted file mode 100644 index 160a941ebe..0000000000 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx +++ /dev/null @@ -1,509 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { components } from 'react-select'; -import ModalBase from '@/_ui/Modal'; -import Select from '@/_ui/Select'; -import SolidIcon from '@/_ui/Icon/SolidIcons'; -import useStore from '@/AppBuilder/_stores/store'; -import { appPermissionService } from '@/_services'; -import { ConfirmDialog } from '@/_components'; -import toast from 'react-hot-toast'; -import Spinner from '@/_ui/Spinner'; -import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; - -const PERMISSION_TYPES = { - single: 'SINGLE', - group: 'GROUP', - all: 'ALL', -}; - -export default function PagePermission({ darkMode }) { - const { moduleId } = useModuleContext(); - const showPagePermissionModal = useStore((state) => state.showPagePermissionModal); - const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); - const editingPage = useStore((state) => state.editingPage); - const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); - const selectedUserGroups = useStore((state) => state.selectedUserGroups); - const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups); - const selectedUsers = useStore((state) => state.selectedUsers); - const setSelectedUsers = useStore((state) => state.setSelectedUsers); - const pagePermission = useStore((state) => state.pagePermission); - const setPagePermission = useStore((state) => state.setPagePermission); - const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions); - - const [pagePermissionType, setPagePermissionType] = useState('all'); - const [showUserGroupSelect, toggleUserGroupSelect] = useState(false); - const [showUsersSelect, toggleUsersSelect] = useState(false); - const [showConfirmDelete, setShowConfirmDelete] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isPermissionsLoading, setPermissionsLoading] = useState(true); - const [initialSelectedGroups, setInitialSelectedGroups] = useState([]); - const [initialSelectedUsers, setInitialSelectedUsers] = useState([]); - const [initalPagePermissionType, setInitialPagePermissionType] = useState('all'); - - useEffect(() => { - if (!showPagePermissionModal) return; - const fetchPagePermission = () => { - appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => { - if (data) { - if (data[0] && data[0]?.type === PERMISSION_TYPES.group) { - const groups = - data[0]?.groups?.map((user) => ({ - label: user?.permissionGroup?.name, - value: user?.permissionGroup?.id, - count: user?.permissionGroup?.count, - })) ?? []; - setPagePermissionType(data[0]?.type?.toLowerCase()); - setInitialPagePermissionType(data[0]?.type?.toLowerCase()); - setPagePermission(data); - toggleUserGroupSelect(true); - setInitialSelectedGroups(groups); - data?.length && setSelectedUserGroups(groups); - } else if (data[0] && data[0]?.type === PERMISSION_TYPES.single) { - const users = - data[0]?.users?.map(({ user }) => { - const firstName = user.firstName || ''; - const lastName = user.lastName || ''; - return { - value: user.id, - label: `${firstName} ${lastName}`.trim(), - email: user.email, - initials: `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase(), - }; - }) ?? []; - setPagePermissionType(data[0]?.type?.toLowerCase()); - setInitialPagePermissionType(data[0]?.type?.toLowerCase()); - setPagePermission(data); - toggleUsersSelect(true); - setInitialSelectedUsers(users); - data?.length && setSelectedUsers(users); - } - } - setPermissionsLoading(false); - }); - }; - fetchPagePermission(); - }, [showPagePermissionModal]); - - const isSelectionUnchanged = useMemo(() => { - if (pagePermissionType === 'group') { - if (!selectedUserGroups.length) return true; - const current = selectedUserGroups - .map((g) => g.value) - .sort() - .join(','); - const initial = initialSelectedGroups - .map((g) => g.value) - .sort() - .join(','); - return current === initial; - } else if (pagePermissionType === 'single') { - if (!selectedUsers.length) return true; - const current = selectedUsers - .map((u) => u.value) - .sort() - .join(','); - const initial = initialSelectedUsers - .map((u) => u.value) - .sort() - .join(','); - return current === initial; - } else { - if (!pagePermission?.length) { - return true; - } else { - return initalPagePermissionType == pagePermissionType; - } - } - }, [ - pagePermissionType, - selectedUserGroups, - initialSelectedGroups, - selectedUsers, - initialSelectedUsers, - initalPagePermissionType, - ]); - - const permissionTypeOptions = useMemo( - () => [ - { - label: 'All users with access to the app', - value: 'all', - icon: 'globe', - }, - { - label: 'Users', - value: 'single', - icon: 'user', - }, - { - label: 'User groups', - value: 'group', - icon: 'usergroup', - }, - ], - [] - ); - const handlePermissionTypeChange = (value) => { - switch (value) { - case 'group': { - toggleUserGroupSelect(true); - toggleUsersSelect(false); - setPagePermissionType('group'); - break; - } - case 'single': { - toggleUsersSelect(true); - toggleUserGroupSelect(false); - setPagePermissionType('single'); - break; - } - case 'all': { - toggleUsersSelect(false); - toggleUserGroupSelect(false); - setPagePermissionType('all'); - } - } - }; - - const handlePagePermissionModalClose = () => { - togglePagePermissionModal(false); - toggleUserGroupSelect(false); - toggleUsersSelect(false); - setPagePermissionType('all'); - setPagePermission(null); - setSelectedUsers([]); - setSelectedUserGroups([]); - setInitialSelectedGroups([]); - setInitialSelectedUsers([]); - }; - - const createPagePermission = () => { - const body = { - pageId: editingPage?.id, - type: PERMISSION_TYPES[pagePermissionType], - ...(pagePermissionType === 'group' - ? { groups: selectedUserGroups.map((group) => group?.value) } - : { users: selectedUsers.map((user) => user?.value) }), - }; - setIsLoading(true); - appPermissionService - .createPagePermission(appId, editingPage?.id, body) - .then((data) => { - toast.success('Permission successfully created!', { - className: 'text-nowrap w-auto mw-100', - }); - updatePageWithPermissions(editingPage?.id, data); - }) - .catch(() => { - toast.error('Permission could not be created. Please try again!', { - className: 'text-nowrap w-auto mw-100', - }); - }) - .finally(() => { - setIsLoading(false); - handlePagePermissionModalClose(); - }); - }; - - const updatePagePermission = () => { - const body = { - pageId: editingPage?.id, - type: PERMISSION_TYPES[pagePermissionType], - ...(pagePermissionType === 'group' - ? { groups: selectedUserGroups.map((group) => group?.value) } - : { users: selectedUsers.map((user) => user?.value) }), - }; - setIsLoading(true); - appPermissionService - .updatePagePermission(appId, editingPage?.id, body) - .then((data) => { - toast.success('Permission successfully updated!', { - className: 'text-nowrap w-auto mw-100', - }); - updatePageWithPermissions(editingPage?.id, data); - }) - .catch(() => { - toast.error('Permission could not be updated. Please try again!', { - className: 'text-nowrap w-auto mw-100', - }); - }) - .finally(() => { - setIsLoading(false); - handlePagePermissionModalClose(); - }); - }; - - const deletePagePermission = () => { - setIsLoading(true); - appPermissionService - .deletePagePermission(appId, editingPage?.id) - .then((data) => { - toast.success('Permission successfully deleted!', { - className: 'text-nowrap w-auto mw-100', - }); - updatePageWithPermissions(editingPage?.id, []); - }) - .catch(() => { - toast.error('Permission could not be deleted. Please try again!', { - className: 'text-nowrap w-auto mw-100', - }); - setShowConfirmDelete(false); - togglePagePermissionModal(true); - }) - .finally(() => { - setIsLoading(false); - setShowConfirmDelete(false); - }); - }; - - const renderPermissionTypeOptions = ({ label, icon }) => { - return ( -
-
- -
-
- {label} -
-
- ); - }; - - return ( - <> - - Page permission -
- } - handleConfirm={!pagePermission ? createPagePermission : updatePagePermission} - show={showPagePermissionModal} - isLoading={isLoading} - handleClose={handlePagePermissionModalClose} - confirmBtnProps={{ - 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" - > -
- {isPermissionsLoading ? ( -
- -
- ) : ( - <> -
-
- -
-
-
-

- Only selected users will be allowed to access this page. Read docs to know more. -

-
-
-
- - -
-
{data.label}
-
{data.count} users
-
-
- - ); - }; - - return ( -
- - -
{data.initials}
-
-
{data.label}
-
{data.email}
-
-
- - ); - }; - - const selectStyles = { - option: (base) => ({ - ...base, - padding: '8px 0px', - }), - }; - return ( -
- - { data-tooltip-dynamic="true" > {decodeEntities(dataQuery.name)} - {' '} + + +
+ {licenseValid && isRestricted && } +
+
{' '} {!isQueryRunnable(dataQuery) && Draft} {localDs && ( <> @@ -143,80 +172,24 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
)} - {!shouldFreeze && isQuerySelected && ( -
-
setRenamingQuery(true)} - > - - - - - -
-
debouncedDuplicateQuery(dataQuery?.id, appId)} - > - - - -
-
- {isDeletingQueryInProcess ? ( -
-
-
- ) : ( - - - - - - - - )} -
- -
- )} +
+ toggleQueryHandlerMenu(true, `query-handler-menu-${dataQuery?.id}`)} + size="small" + variant="outline" + className="" + id={`query-handler-menu-${dataQuery?.id}`} + /> +
setShowDeleteConfirmation(false)} + onCancel={() => deleteDataQuery(null)} darkMode={darkMode} /> diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx new file mode 100644 index 0000000000..a9e3030b51 --- /dev/null +++ b/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx @@ -0,0 +1,174 @@ +import React, { useCallback } from 'react'; +import { Overlay, Popover } from 'react-bootstrap'; +import useStore from '@/AppBuilder/_stores/store'; +import classNames from 'classnames'; +import Edit from '@/_ui/Icon/bulkIcons/Edit'; +import Trash from '@/_ui/Icon/solidIcons/Trash'; +import Copy from '@/_ui/Icon/solidIcons/Copy'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { shallow } from 'zustand/shallow'; +import { ToolTip } from '@/_components/ToolTip'; +import { debounce } from 'lodash'; +import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; + +const QueryCardMenu = ({ darkMode }) => { + const { moduleId } = useModuleContext(); + const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); + const selectedQuery = useStore((state) => state.queryPanel.selectedQuery); + const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal); + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; + const targetBtnForMenu = useStore((state) => state.queryPanel.targetBtnForMenu); + const targetElement = document.getElementById(targetBtnForMenu); + const showQueryHandlerMenu = useStore((state) => state.queryPanel.showQueryHandlerMenu); + const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu); + const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery); + const setPreviewData = useStore((state) => state.queryPanel.setPreviewData); + const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery); + const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery); + + const QUERY_MENU_OPTIONS = [ + { + label: 'Rename', + value: 'rename', + icon: , + showTooltip: false, + }, + { + label: 'Duplicate', + value: 'duplicate', + icon: , + showTooltip: false, + }, + { + label: 'Query permission', + value: 'permission', + icon: ( + permission-icon + ), + trailingIcon: , + }, + { + label: 'Delete', + value: 'delete', + icon: , + showTooltip: false, + }, + ]; + + // To prevent user clicking from continuous clicks + const debouncedDuplicateQuery = useCallback( + debounce((queryId, appId) => { + duplicateQuery(queryId, appId); + setPreviewData(null); + }, 500), + [duplicateQuery] + ); + + const handleQueryMenuActions = (value) => { + if (value === 'rename') { + setRenamingQuery(selectedQuery?.id); + } + if (value === 'duplicate') { + debouncedDuplicateQuery(selectedQuery?.id, appId); + } + if (value === 'permission') { + if (!licenseValid) return; + toggleQueryPermissionModal(true); + } + if (value === 'delete') { + deleteDataQuery(selectedQuery?.id); + } + toggleQueryHandlerMenu(false); + }; + + usePopoverObserver( + document.getElementsByClassName('query-list')[0], + targetElement, + document.getElementById('query-list-menu'), + showQueryHandlerMenu, + () => (document.getElementById('query-list-menu').style.display = 'block'), + () => (document.getElementById('query-list-menu').style.display = 'none') + ); + + return ( + toggleQueryHandlerMenu(false)} + popperConfig={{ + modifiers: [ + { + name: 'flip', + options: { + fallbackPlacements: ['top-start'], + flipVariations: true, + allowedAutoPlacements: ['top', 'bottom'], + boundary: 'viewport', + }, + }, + { + name: 'offset', + options: { + offset: [0, 3], + }, + }, + ], + }} + > + {(props) => ( + + + {QUERY_MENU_OPTIONS.map((option) => { + const optionBody = ( +
{ + e.stopPropagation(); + handleQueryMenuActions(option.value); + }} + > +
{option.icon}
+
+ {option?.label} +
+ {option.value === 'permission' && !licenseValid && option.trailingIcon && option.trailingIcon} +
+ ); + + return option.value === 'permission' ? ( + + {optionBody} + + ) : ( + optionBody + ); + })} +
+
+ )} +
+ ); +}; + +export default QueryCardMenu; diff --git a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx index 9ac052ae51..97b4daa68f 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx @@ -16,6 +16,10 @@ import DataSourceSelect from '../QueryManager/Components/DataSourceSelect'; import { OverlayTrigger, Popover } from 'react-bootstrap'; import FolderEmpty from '@/_ui/Icon/solidIcons/FolderEmpty'; import useStore from '@/AppBuilder/_stores/store'; +import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal'; +import { shallow } from 'zustand/shallow'; +import { appPermissionService } from '@/_services'; +import QueryCardMenu from './QueryCardMenu'; export const QueryDataPane = ({ darkMode }) => { const { t } = useTranslation(); @@ -34,6 +38,12 @@ export const QueryDataPane = ({ darkMode }) => { function isDataSourceLocal(dataQuery) { return dataSources.some((dataSource) => dataSource.id === dataQuery.data_source_id); } + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; + const selectedQuery = useStore((state) => state.queryPanel.selectedQuery); + const showQueryPermissionModal = useStore((state) => state.queryPanel.showQueryPermissionModal); + const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal); + const setQueries = useStore((state) => state.dataQuery.setQueries); useEffect(() => { setQueryPanelSearchTerm(searchTermForFilters); @@ -171,6 +181,33 @@ export const QueryDataPane = ({ darkMode }) => { {filteredQueries.map((query) => ( ))} + + {licenseValid && ( + appPermissionService.getQueryPermission(appId, id)} + createPermission={(id, appId, body) => appPermissionService.createQueryPermission(appId, id, body)} + updatePermission={(id, appId, body) => appPermissionService.updateQueryPermission(appId, id, body)} + deletePermission={(id, appId) => appPermissionService.deleteQueryPermission(appId, id)} + onSuccess={(data) => { + const updatedDataQueries = dataQueries.map((query) => { + if (query.id === selectedQuery.id) { + return { + ...query, + permissions: data.length === 0 || data.length === undefined ? [] : [data[0]], + }; + } + return query; + }); + setQueries(updatedDataQueries); + }} + /> + )} normalizeQueryTransformationOptions(query)); setQueries(dataQueries, moduleId); diff --git a/frontend/src/AppBuilder/_stores/slices/appSlice.js b/frontend/src/AppBuilder/_stores/slices/appSlice.js index 6962c6752d..fdb81e97a9 100644 --- a/frontend/src/AppBuilder/_stores/slices/appSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/appSlice.js @@ -16,6 +16,11 @@ const initialState = { pageSwitchInProgress: false, isTJDarkMode: localStorage.getItem('darkMode') === 'true', isViewer: false, + isComponentLayoutReady: false, + appPermission: { + selectedUsers: [], + selectedUserGroups: [], + }, appStore: { modules: { canvas: { @@ -276,4 +281,12 @@ export const createAppSlice = (set, get) => ({ return get().appStore.modules[moduleId].app.homePageId; }, updateIsTJDarkMode: (newMode) => set({ isTJDarkMode: newMode }, false, 'updateIsTJDarkMode'), + setSelectedUserGroups: (groups) => + set((state) => { + state.appPermission.selectedUserGroups = groups; + }), + setSelectedUsers: (users) => + set((state) => { + state.appPermission.selectedUsers = users; + }), }); diff --git a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js index 99d1ee660b..34bea1bf84 100644 --- a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js +++ b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js @@ -1,4 +1,4 @@ -import { dataqueryService } from '@/_services'; +import { dataqueryService, appPermissionService } from '@/_services'; import { getDefaultOptions } from '@/_stores/storeHelper'; import { v4 as uuidv4 } from 'uuid'; import _, { isEmpty, throttle } from 'lodash'; @@ -270,7 +270,6 @@ export const createDataQuerySlice = (set, get) => ({ ) .then((data) => { set((state) => { - state.dataQuery.creatingQueryInProcessId = null; state.dataQuery.queries.modules[moduleId] = [ { ...data, @@ -308,6 +307,42 @@ export const createDataQuerySlice = (set, get) => ({ }; createAppVersionEventHandlers(newEvent, moduleId); }); + + if (queryToClone.permissions && queryToClone.permissions.length !== 0) { + const body = { + type: queryToClone.permissions[0]?.type, + ...(queryToClone.permissions[0]?.type === 'GROUP' + ? { + groups: (queryToClone.permissions[0]?.groups || queryToClone.permissions[0]?.users || []).map( + (group) => group.permissionGroupsId || group.permission_groups_id + ), + } + : { users: queryToClone.permissions[0]?.users.map((user) => user.userId || user.user_id) }), + }; + appPermissionService + .createQueryPermission(appId, data.id, body) + .then((newQuery) => { + const dataQueries = get().dataQuery.queries.modules[moduleId]; + const updatedDataQueries = dataQueries.map((query) => { + if (query.id === data.id) { + return { + ...query, + permissions: newQuery.length === 0 || newQuery.length === undefined ? [] : [newQuery[0]], + }; + } + return query; + }); + get().dataQuery.setQueries(updatedDataQueries); + }) + .catch(() => { + toast.error('Permission could not be created. Please try again!', { + className: 'text-nowrap w-auto mw-100', + }); + }); + } + set((state) => { + state.dataQuery.creatingQueryInProcessId = null; + }); }) .catch((error) => { console.error('error', error); @@ -438,7 +473,10 @@ export const createDataQuerySlice = (set, get) => ({ const queries = get().dataQuery.queries.modules[moduleId]; try { for (const query of queries) { - if ((query.options.runOnPageLoad || query.options.run_on_page_load) && isQueryRunnable(query)) { + if ( + (query.options?.runOnPageLoad || query.options?.run_on_page_load) && + (query.restricted || isQueryRunnable(query)) + ) { await get().queryPanel.runQuery( query.id, query.name, diff --git a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js index 5ece338ed8..8c67ab9825 100644 --- a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js @@ -100,10 +100,6 @@ export const createPageMenuSlice = (set, get) => { pageSettingSelected: false, pageSettings: {}, showPagePermissionModal: false, - permissionPage: null, - selectedUserGroups: [], - selectedUsers: [], - pagePermission: null, toggleSearch: (show) => set((state) => { @@ -432,26 +428,12 @@ export const createPageMenuSlice = (set, get) => { } }, - setPagePermission: (pagePermission) => - set((state) => { - state.pagePermission = pagePermission; - }), - togglePagePermissionModal: (show) => { set((state) => { state.showPagePermissionModal = show; }); }, - setSelectedUserGroups: (groups) => - set((state) => { - state.selectedUserGroups = groups; - }), - - setSelectedUsers: (users) => - set((state) => { - state.selectedUsers = users; - }), setEditingPage: (page) => set((state) => { state.editingPage = page; diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js index f33f67c66e..903b548ca2 100644 --- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js @@ -26,6 +26,12 @@ const initialState = { loadingDataQueries: false, isPreviewQueryLoading: false, queryPanelSearchTem: '', + showQueryPermissionModal: false, + targetBtnForMenu: null, + showQueryHandlerMenu: false, + showDeleteConfirmation: false, + renamingQueryId: null, + deletingQueryId: null, }; export const createQueryPanelSlice = (set, get) => ({ @@ -223,6 +229,7 @@ export const createQueryPanelSlice = (set, get) => ({ selectedEnvironment, isPublicAccess, currentVersionId, + currentMode, } = get(); const { queryPreviewData, @@ -352,14 +359,15 @@ export const createQueryPanelSlice = (set, get) => ({ let queryExecutionPromise = null; if (query.kind === 'runjs') { - queryExecutionPromise = executeMultilineJS(query.options.code, query?.id, false, mode, parameters, moduleId); + queryExecutionPromise = executeMultilineJS(query.options?.code, query?.id, false, mode, parameters, moduleId); } else if (query.kind === 'runpy') { - queryExecutionPromise = executeRunPycode(query.options.code, query, false, mode, queryState, moduleId); + queryExecutionPromise = executeRunPycode(query.options?.code, query, false, mode, queryState, moduleId); } else if (query.kind === 'workflows') { queryExecutionPromise = executeWorkflow( moduleId, - query.options.workflowId, - query.options.blocking, + query, + query.options?.workflowId, + query.options?.blocking, query.options?.params, (currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id //TODO: currentAppEnvironmentId may no longer required. Need to check ); @@ -374,7 +382,8 @@ export const createQueryPanelSlice = (set, get) => ({ options, query?.options, versionId, - !isPublicAccess ? (currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id : undefined //TODO: currentAppEnvironmentId may no longer required. Need to check + !isPublicAccess ? (currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id : undefined, //TODO: currentAppEnvironmentId may no longer required. Need to check + currentMode ); } @@ -447,7 +456,7 @@ export const createQueryPanelSlice = (set, get) => ({ queryId, { isLoading: false, - ...(query.kind === 'restapi' + ...(query.kind === 'restapi' || data.data.type === 'tj-401' ? { metadata: data.metadata, request: data.data.requestObject, @@ -725,6 +734,28 @@ export const createQueryPanelSlice = (set, get) => ({ const { queryPanel: { evaluatePythonCode }, } = get(); + + if (query.restricted) { + return { + status: 'failed', + message: 'Query could not be completed', + description: 'Response code 401 (Unauthorized)', + data: { + type: 'tj-401', + responseObject: { + statusCode: 401, + responseBody: 'Unauthorized Access', + }, + }, + metadata: { + response: { + statusCode: 401, + responseBody: 'Unauthorized Access', + }, + }, + }; + } + return { data: await evaluatePythonCode({ code, query, isPreview, mode, currentState }) }; }, @@ -948,12 +979,33 @@ export const createQueryPanelSlice = (set, get) => ({ // queries: updatedQueries, // }); }, - executeWorkflow: async (moduleId = 'canvas', workflowId, _blocking = false, params = {}, appEnvId) => { + executeWorkflow: async (moduleId = 'canvas', query, workflowId, _blocking = false, params = {}, appEnvId) => { const { getAppId, getAllExposedValues } = get(); const appId = getAppId('canvas'); const currentState = getAllExposedValues(moduleId); const resolvedParams = get().resolveReferences(moduleId, params, currentState, {}, {}); + if (query.restricted) { + return { + status: 'failed', + message: 'Query could not be completed', + description: 'Response code 401 (Unauthorized)', + data: { + type: 'tj-401', + responseObject: { + statusCode: 401, + responseBody: 'Unauthorized Access', + }, + }, + metadata: { + response: { + statusCode: 401, + responseBody: 'Unauthorized Access', + }, + }, + }; + } + try { const response = await workflowExecutionsService.execute(workflowId, resolvedParams, appId, appEnvId); return { data: response.result, status: 'ok' }; @@ -1004,6 +1056,27 @@ export const createQueryPanelSlice = (set, get) => ({ const queryDetails = dataQuery.queries.modules?.[moduleId].find((q) => q.id === queryId); + if (queryDetails.restricted) { + return { + status: 'failed', + message: 'Query could not be completed', + description: 'Response code 401 (Unauthorized)', + data: { + type: 'tj-401', + responseObject: { + statusCode: 401, + responseBody: 'Unauthorized Access', + }, + }, + metadata: { + response: { + statusCode: 401, + responseBody: 'Unauthorized Access', + }, + }, + }; + } + const defaultParams = queryDetails?.options?.parameters?.reduce( (paramObj, param) => ({ @@ -1154,5 +1227,24 @@ export const createQueryPanelSlice = (set, get) => ({ }; previewQuery(query, false, undefined, moduleId); }, + toggleQueryPermissionModal: (show) => { + set((state) => { + state.queryPanel.showQueryPermissionModal = show; + }); + }, + toggleQueryHandlerMenu: (show, id) => { + set((state) => { + if (show) state.queryPanel.targetBtnForMenu = id; + state.queryPanel.showQueryHandlerMenu = show; + }); + }, + setRenamingQuery: (queryId) => + set((state) => { + state.queryPanel.renamingQueryId = queryId; + }), + deleteDataQuery: (queryId) => + set((state) => { + state.queryPanel.deletingQueryId = queryId; + }), }, }); diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index b0d698398e..b7b2a5980b 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -381,7 +381,7 @@ class HomePageComponent extends React.Component { } }; - importFile = async (importJSON, appName, skipPagePermissionsGroupCheck = false) => { + importFile = async (importJSON, appName, skipPermissionsGroupCheck = false) => { this.setState({ isImportingApp: true }); // For backward compatibility with legacy app import const organization_id = this.state.currentUser?.organization_id; @@ -397,7 +397,7 @@ class HomePageComponent extends React.Component { const requestBody = { organization_id, ...importJSON, - skip_page_permissions_group_check: skipPagePermissionsGroupCheck, + skip_permissions_group_check: skipPermissionsGroupCheck, }; let installedPluginsInfo = []; try { diff --git a/frontend/src/ToolJetUI/List/list.scss b/frontend/src/ToolJetUI/List/list.scss index c5aa03aada..04258a0788 100644 --- a/frontend/src/ToolJetUI/List/list.scss +++ b/frontend/src/ToolJetUI/List/list.scss @@ -64,6 +64,7 @@ button:focus:not(:focus-visible) { padding: 10px 14px; cursor: pointer; display: flex; + align-items: center; &:hover { background-color: var(--slate3); @@ -78,6 +79,10 @@ button:focus:not(:focus-visible) { .color-tomato9 { color: var(--tomato9) } + + .color-disabled { + color: var(--text-disabled); + } } } diff --git a/frontend/src/_services/appPermission.service.js b/frontend/src/_services/appPermission.service.js index 85cb6ee004..fd2438f058 100644 --- a/frontend/src/_services/appPermission.service.js +++ b/frontend/src/_services/appPermission.service.js @@ -7,6 +7,10 @@ export const appPermissionService = { createPagePermission, updatePagePermission, deletePagePermission, + getQueryPermission, + createQueryPermission, + updateQueryPermission, + deleteQueryPermission, }; function getPagePermission(appId, pageId) { @@ -47,3 +51,41 @@ function deletePagePermission(appId, pageId) { }; return fetch(`${config.apiUrl}/app-permissions/${appId}/pages/${pageId}`, requestOptions).then(handleResponse); } + +function getQueryPermission(appId, queryId) { + const requestOptions = { + method: 'GET', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse); +} + +function createQueryPermission(appId, queryId, body) { + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse); +} + +function updateQueryPermission(appId, queryId, body) { + const requestOptions = { + method: 'PUT', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse); +} + +function deleteQueryPermission(appId, queryId) { + const requestOptions = { + method: 'DELETE', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse); +} diff --git a/frontend/src/_services/dataquery.service.js b/frontend/src/_services/dataquery.service.js index 3919a6b0f3..3dc5e26593 100644 --- a/frontend/src/_services/dataquery.service.js +++ b/frontend/src/_services/dataquery.service.js @@ -13,9 +13,9 @@ export const dataqueryService = { bulkUpdateQueryOptions, }; -function getAll(appVersionId) { +function getAll(appVersionId, mode) { const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' }; - return fetch(`${config.apiUrl}/data-queries/${appVersionId}`, requestOptions).then(handleResponse); + return fetch(`${config.apiUrl}/data-queries/${appVersionId}?mode=${mode}`, requestOptions).then(handleResponse); } function create(app_id, app_version_id, name, kind, options, data_source_id, plugin_id) { @@ -72,7 +72,7 @@ function del(id, versionId) { return fetch(`${config.apiUrl}/data-queries/${id}/versions/${versionId}`, requestOptions).then(handleResponse); } -function run(queryId, resolvedOptions, options, versionId, environmentId) { +function run(queryId, resolvedOptions, options, versionId, environmentId, mode) { const body = { resolvedOptions: resolvedOptions, options: options, @@ -80,7 +80,7 @@ function run(queryId, resolvedOptions, options, versionId, environmentId) { let url = `${config.apiUrl}/data-queries/${queryId}/versions/${versionId}/run${ environmentId && environmentId !== 'undefined' ? `/${environmentId}` : '' - }`; + }?mode=${mode}`; //For public/released apps if (!environmentId || !versionId) { diff --git a/frontend/src/_styles/queryManager.scss b/frontend/src/_styles/queryManager.scss index 1fed6a3a13..861dbd78fe 100644 --- a/frontend/src/_styles/queryManager.scss +++ b/frontend/src/_styles/queryManager.scss @@ -166,7 +166,7 @@ $border-radius: 4px; } .query-row:hover .query-rename-delete-btn { - display: flex; + display: flex !important; } .query-row { @@ -189,12 +189,10 @@ $border-radius: 4px; } .query-rename-delete-btn { - display: none; align-items: center; justify-content: flex-end; - gap: 8px; margin-left: 2px; - width: 66px; + width: 22px; height: 20px; } diff --git a/frontend/src/_ui/Icon/solidIcons/MoreVertical01.jsx b/frontend/src/_ui/Icon/solidIcons/MoreVertical01.jsx new file mode 100644 index 0000000000..dbda2756d5 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/MoreVertical01.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const MoreVertical01 = ({ fill = '#11181C', width = '12', height = '13', className = '', viewBox = '0 0 12 13' }) => ( + + + +); + +export default MoreVertical01; diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js index 0e7192edaf..c6fb056b78 100644 --- a/frontend/src/_ui/Icon/solidIcons/index.js +++ b/frontend/src/_ui/Icon/solidIcons/index.js @@ -239,6 +239,7 @@ import EnterpriseCrown from './EnterrpiseCrown.jsx'; import FileCode from './FileCode.jsx'; import Corners from './Corners.jsx'; import Moon from './Moon.jsx'; +import MoreVertical01 from './MoreVertical01.jsx'; import RemoveFolder from './RemoveFolder.jsx'; const Icon = (props) => { @@ -725,6 +726,8 @@ const Icon = (props) => { return ; case 'moon': return ; + case 'morevertical01': + return ; default: return ; } diff --git a/frontend/src/modules/Appbuilder/components/AppPermissionsModal/AppPermissionsModal.jsx b/frontend/src/modules/Appbuilder/components/AppPermissionsModal/AppPermissionsModal.jsx new file mode 100644 index 0000000000..75be7509b2 --- /dev/null +++ b/frontend/src/modules/Appbuilder/components/AppPermissionsModal/AppPermissionsModal.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent'; + +const AppPermissionsModal = () => { + return <>; +}; + +export default withEditionSpecificComponent(AppPermissionsModal, 'Appbuilder'); diff --git a/frontend/src/modules/Appbuilder/components/AppPermissionsModal/index.js b/frontend/src/modules/Appbuilder/components/AppPermissionsModal/index.js new file mode 100644 index 0000000000..cae7417742 --- /dev/null +++ b/frontend/src/modules/Appbuilder/components/AppPermissionsModal/index.js @@ -0,0 +1 @@ +export { default } from './AppPermissionsModal'; diff --git a/frontend/src/modules/Appbuilder/components/index.js b/frontend/src/modules/Appbuilder/components/index.js index 196fb3b07b..8913fdae56 100644 --- a/frontend/src/modules/Appbuilder/components/index.js +++ b/frontend/src/modules/Appbuilder/components/index.js @@ -4,6 +4,7 @@ import LogoNavDropdown from './LogoNavDropdown'; import AppEnvironments from './AppEnvironments'; import ThemeSelect from './ThemeSelect'; import ColorSwatches from './ColorSwatches'; +import AppPermissionsModal from './AppPermissionsModal'; import ComponentModuleTab from './ComponentModuleTab'; export { @@ -13,5 +14,6 @@ export { AppEnvironments, ThemeSelect, ColorSwatches, + AppPermissionsModal, ComponentModuleTab, }; diff --git a/server/ee b/server/ee index 9e988341ae..8df193d06e 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 9e988341aeb7be4348bd7d893a4389507785281c +Subproject commit 8df193d06e146f72447a3fa95de4544cd47080d5 diff --git a/server/migrations/1747759439358-CreateQueryPermissions.ts b/server/migrations/1747759439358-CreateQueryPermissions.ts new file mode 100644 index 0000000000..fe6bce52ce --- /dev/null +++ b/server/migrations/1747759439358-CreateQueryPermissions.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +export class CreateQueryPermissions1747759439358 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'query_permissions', + columns: [ + { + name: 'id', + type: 'uuid', + isGenerated: true, + default: 'gen_random_uuid()', + isPrimary: true, + }, + { + name: 'query_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'type', + type: 'enum', + enum: ['SINGLE', 'GROUP'], + }, + { + name: 'created_at', + type: 'timestamp', + isNullable: false, + default: 'now()', + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'query_permissions', + new TableForeignKey({ + columnNames: ['query_id'], + referencedColumnNames: ['id'], + referencedTableName: 'data_queries', + onDelete: 'CASCADE', + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('query_permissions'); + } +} diff --git a/server/migrations/1747763331564-CreateQueryUsers.ts b/server/migrations/1747763331564-CreateQueryUsers.ts new file mode 100644 index 0000000000..819b736fba --- /dev/null +++ b/server/migrations/1747763331564-CreateQueryUsers.ts @@ -0,0 +1,76 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +export class CreateQueryUsers1747763331564 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'query_users', + columns: [ + { + name: 'id', + type: 'uuid', + isGenerated: true, + default: 'gen_random_uuid()', + isPrimary: true, + }, + { + name: 'query_permissions_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'user_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'permission_groups_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamp', + isNullable: false, + default: 'now()', + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'query_users', + new TableForeignKey({ + columnNames: ['query_permissions_id'], + referencedColumnNames: ['id'], + referencedTableName: 'query_permissions', + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createForeignKey( + 'query_users', + new TableForeignKey({ + columnNames: ['user_id'], + referencedColumnNames: ['id'], + referencedTableName: 'users', + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createForeignKey( + 'query_users', + new TableForeignKey({ + columnNames: ['permission_groups_id'], + referencedColumnNames: ['id'], + referencedTableName: 'permission_groups', + onDelete: 'CASCADE', + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('query_users'); + } +} diff --git a/server/src/dto/import-resources.dto.ts b/server/src/dto/import-resources.dto.ts index 89b3ee182c..3ad6d2cee0 100644 --- a/server/src/dto/import-resources.dto.ts +++ b/server/src/dto/import-resources.dto.ts @@ -31,7 +31,7 @@ export class ImportResourcesDto { @IsOptional() @IsBoolean() - skip_page_permissions_group_check?: boolean; + skip_permissions_group_check?: boolean; } export class ImportAppDto { diff --git a/server/src/entities/data_query.entity.ts b/server/src/entities/data_query.entity.ts index 49e5b1f1a5..7e3a8d25a5 100644 --- a/server/src/entities/data_query.entity.ts +++ b/server/src/entities/data_query.entity.ts @@ -10,11 +10,13 @@ import { JoinTable, ManyToMany, AfterLoad, + OneToMany, } from 'typeorm'; import { App } from './app.entity'; import { AppVersion } from './app_version.entity'; import { DataSource } from './data_source.entity'; import { Plugin } from './plugin.entity'; +import { QueryPermission } from './query_permissions.entity'; @Entity({ name: 'data_queries' }) export class DataQuery extends BaseEntity { @@ -81,6 +83,9 @@ export class DataQuery extends BaseEntity { app: App; + @OneToMany(() => QueryPermission, (permission) => permission.query) + permissions: QueryPermission[]; + @AfterLoad() updatePlugin() { if (this.plugins?.length) this.plugin = this.plugins[0]; diff --git a/server/src/entities/group_permissions.entity.ts b/server/src/entities/group_permissions.entity.ts index d321d84b5f..26667796ae 100644 --- a/server/src/entities/group_permissions.entity.ts +++ b/server/src/entities/group_permissions.entity.ts @@ -14,6 +14,7 @@ import { GroupUsers } from './group_users.entity'; import { GranularPermissions } from './granular_permissions.entity'; import { GROUP_PERMISSIONS_TYPE } from '@modules/group-permissions/constants'; import { PageUser } from './page_users.entity'; +import { QueryUser } from './query_users.entity'; @Entity({ name: 'permission_groups' }) export class GroupPermissions extends BaseEntity { @@ -72,5 +73,8 @@ export class GroupPermissions extends BaseEntity { @OneToMany(() => PageUser, (pageUser) => pageUser.permissionGroup) pageUsers: PageUser[]; + @OneToMany(() => QueryUser, (queryUser) => queryUser.permissionGroup) + queryUsers: QueryUser[]; + disabled?: boolean; } diff --git a/server/src/entities/query_permissions.entity.ts b/server/src/entities/query_permissions.entity.ts new file mode 100644 index 0000000000..c693c6b994 --- /dev/null +++ b/server/src/entities/query_permissions.entity.ts @@ -0,0 +1,29 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, OneToMany } from 'typeorm'; +import { DataQuery } from './data_query.entity'; +import { QueryUser } from './query_users.entity'; +import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants'; + +@Entity('query_permissions') +export class QueryPermission { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'query_id', type: 'uuid', nullable: false }) + queryId: string; + + @Column({ + type: 'enum', + enum: PAGE_PERMISSION_TYPE, + }) + type: PAGE_PERMISSION_TYPE; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @ManyToOne(() => DataQuery, (query) => query.permissions, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'query_id' }) + query: DataQuery; + + @OneToMany(() => QueryUser, (queryUser) => queryUser.queryPermission) + users: QueryUser[]; +} diff --git a/server/src/entities/query_users.entity.ts b/server/src/entities/query_users.entity.ts new file mode 100644 index 0000000000..46b16be0af --- /dev/null +++ b/server/src/entities/query_users.entity.ts @@ -0,0 +1,34 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm'; +import { User } from './user.entity'; +import { QueryPermission } from './query_permissions.entity'; +import { GroupPermissions } from './group_permissions.entity'; + +@Entity('query_users') +export class QueryUser { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'query_permissions_id', type: 'uuid' }) + queryPermissionsId: string; + + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + userId: string | null; + + @Column({ name: 'permission_groups_id', type: 'uuid', nullable: true }) + permissionGroupsId: string | null; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @ManyToOne(() => QueryPermission, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'query_permissions_id' }) + queryPermission: QueryPermission; + + @ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => GroupPermissions, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'permission_groups_id' }) + permissionGroup: GroupPermissions; +} diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index e052e11245..515b1f469e 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -30,6 +30,7 @@ import { AiConversation } from './ai_conversation.entity'; import { AiResponseVote } from './ai_response_vote.entity'; import { USER_ROLE } from '@modules/group-permissions/constants'; import { PageUser } from './page_users.entity'; +import { QueryUser } from './query_users.entity'; @Entity({ name: 'users' }) export class User extends BaseEntity { @@ -188,6 +189,9 @@ export class User extends BaseEntity { @OneToMany(() => PageUser, (pageUser) => pageUser.user) pageUsers: PageUser[]; + @OneToMany(() => QueryUser, (queryUser) => queryUser.user) + queryUsers: QueryUser[]; + organizationId: string; invitedOrganizationId: string; organizationIds?: Array; diff --git a/server/src/helpers/error_type.constant.ts b/server/src/helpers/error_type.constant.ts index cb483e8896..e34ce5b69d 100644 --- a/server/src/helpers/error_type.constant.ts +++ b/server/src/helpers/error_type.constant.ts @@ -1,7 +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_GROUP_ERROR: 'Following groups are missing from the workspace', PERMISSION_CHECK: 'permission-check', }, }; diff --git a/server/src/modules/app-permissions/ability/index.ts b/server/src/modules/app-permissions/ability/index.ts index d2e8c263b2..5a03f417d5 100644 --- a/server/src/modules/app-permissions/ability/index.ts +++ b/server/src/modules/app-permissions/ability/index.ts @@ -38,6 +38,10 @@ export class FeatureAbilityFactory extends AbilityFactory FEATURE_KEY.CREATE_PAGE_PERMISSIONS, FEATURE_KEY.UPDATE_PAGE_PERMISSIONS, FEATURE_KEY.DELETE_PAGE_PERMISSIONS, + FEATURE_KEY.FETCH_QUERY_PERMISSIONS, + FEATURE_KEY.CREATE_QUERY_PERMISSIONS, + FEATURE_KEY.UPDATE_QUERY_PERMISSIONS, + FEATURE_KEY.DELETE_QUERY_PERMISSIONS, ], App ); @@ -56,6 +60,10 @@ export class FeatureAbilityFactory extends AbilityFactory FEATURE_KEY.CREATE_PAGE_PERMISSIONS, FEATURE_KEY.UPDATE_PAGE_PERMISSIONS, FEATURE_KEY.DELETE_PAGE_PERMISSIONS, + FEATURE_KEY.FETCH_QUERY_PERMISSIONS, + FEATURE_KEY.CREATE_QUERY_PERMISSIONS, + FEATURE_KEY.UPDATE_QUERY_PERMISSIONS, + FEATURE_KEY.DELETE_QUERY_PERMISSIONS, ], App ); @@ -66,7 +74,15 @@ export class FeatureAbilityFactory extends AbilityFactory isAllAppsViewable || (userAppPermissions?.viewableAppsId?.length && appId && userAppPermissions.viewableAppsId.includes(appId)) ) { - can([FEATURE_KEY.FETCH_USERS, FEATURE_KEY.FETCH_USER_GROUPS, FEATURE_KEY.FETCH_PAGE_PERMISSIONS], App); + can( + [ + FEATURE_KEY.FETCH_USERS, + FEATURE_KEY.FETCH_USER_GROUPS, + FEATURE_KEY.FETCH_PAGE_PERMISSIONS, + FEATURE_KEY.FETCH_QUERY_PERMISSIONS, + ], + App + ); } } } diff --git a/server/src/modules/app-permissions/constants/features.ts b/server/src/modules/app-permissions/constants/features.ts index 6d77625ec5..360b1cf4c9 100644 --- a/server/src/modules/app-permissions/constants/features.ts +++ b/server/src/modules/app-permissions/constants/features.ts @@ -10,5 +10,9 @@ export const FEATURES: FeaturesConfig = { [FEATURE_KEY.CREATE_PAGE_PERMISSIONS]: {}, [FEATURE_KEY.UPDATE_PAGE_PERMISSIONS]: {}, [FEATURE_KEY.DELETE_PAGE_PERMISSIONS]: {}, + [FEATURE_KEY.FETCH_QUERY_PERMISSIONS]: {}, + [FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: {}, + [FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: {}, + [FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: {}, }, }; diff --git a/server/src/modules/app-permissions/constants/index.ts b/server/src/modules/app-permissions/constants/index.ts index c1d2afe78b..ff5063e948 100644 --- a/server/src/modules/app-permissions/constants/index.ts +++ b/server/src/modules/app-permissions/constants/index.ts @@ -4,6 +4,12 @@ export enum PAGE_PERMISSION_TYPE { ALL = 'ALL', } +export enum PERMISSION_ENTITY_TYPE { + PAGE = 'PAGE', + QUERY = 'QUERY', + COMPONENT = 'COMPONENT', +} + export enum FEATURE_KEY { FETCH_USERS = 'fetch_users', FETCH_USER_GROUPS = 'fetch_user_groups', @@ -11,4 +17,8 @@ export enum FEATURE_KEY { CREATE_PAGE_PERMISSIONS = 'create_page_permissions', UPDATE_PAGE_PERMISSIONS = 'update_page_permissions', DELETE_PAGE_PERMISSIONS = 'delete_page_permissions', + FETCH_QUERY_PERMISSIONS = 'fetch_query_permissions', + CREATE_QUERY_PERMISSIONS = 'create_query_permissions', + UPDATE_QUERY_PERMISSIONS = 'update_query_permissions', + DELETE_QUERY_PERMISSIONS = 'delete_query_permissions', } diff --git a/server/src/modules/app-permissions/controller.ts b/server/src/modules/app-permissions/controller.ts index 2d0ccea9ce..d317e3115b 100644 --- a/server/src/modules/app-permissions/controller.ts +++ b/server/src/modules/app-permissions/controller.ts @@ -8,7 +8,7 @@ import { MODULES } from '@modules/app/constants/modules'; import { InitFeature } from '@modules/app/decorators/init-feature.decorator'; import { FEATURE_KEY } from './constants'; import { JwtAuthGuard } from '@modules/session/guards/jwt-auth.guard'; -import { CreatePagePermissionDto } from './dto'; +import { CreatePermissionDto } from './dto'; @InitModule(MODULES.APP_PERMISSIONS) @UseGuards(JwtAuthGuard, FeatureAbilityGuard) @@ -53,7 +53,7 @@ export class AppPermissionsController implements IAppPermissionsController { @User() user, @Param('appId') appId: string, @Param('pageId') pageId: string, - @Body() body: CreatePagePermissionDto, + @Body() body: CreatePermissionDto, @Res({ passthrough: true }) response: Response ): Promise { throw new NotFoundException(); @@ -65,7 +65,7 @@ export class AppPermissionsController implements IAppPermissionsController { @User() user, @Param('appId') appId: string, @Param('pageId') pageId: string, - @Body() body: CreatePagePermissionDto, + @Body() body: CreatePermissionDto, @Res({ passthrough: true }) response: Response ): Promise { throw new NotFoundException(); @@ -81,4 +81,50 @@ export class AppPermissionsController implements IAppPermissionsController { ): Promise { throw new NotFoundException(); } + + @InitFeature(FEATURE_KEY.FETCH_QUERY_PERMISSIONS) + @Get(':appId/queries/:queryId') + async fetchQueryPermissions( + @User() user, + @Param('appId') appId: string, + @Param('queryId') queryId: string, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.CREATE_QUERY_PERMISSIONS) + @Post(':appId/queries/:queryId') + async createQueryPermissions( + @User() user, + @Param('appId') appId: string, + @Param('queryId') queryId: string, + @Body() body: CreatePermissionDto, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.UPDATE_QUERY_PERMISSIONS) + @Put(':appId/queries/:queryId') + async updateQueryPermissions( + @User() user, + @Param('appId') appId: string, + @Param('queryId') queryId: string, + @Body() body: CreatePermissionDto, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.DELETE_QUERY_PERMISSIONS) + @Delete(':appId/queries/:queryId') + async deleteQueryPermissions( + @User() user, + @Param('appId') appId: string, + @Param('queryId') queryId: string, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } } diff --git a/server/src/modules/app-permissions/dto/index.ts b/server/src/modules/app-permissions/dto/index.ts index 20a1bd98b8..b32fafe0a1 100644 --- a/server/src/modules/app-permissions/dto/index.ts +++ b/server/src/modules/app-permissions/dto/index.ts @@ -2,10 +2,10 @@ import { IsUUID, IsEnum, IsArray, IsString, IsOptional, ValidateIf } from 'class import { Type } from 'class-transformer'; import { PAGE_PERMISSION_TYPE } from '../constants'; -export class CreatePagePermissionDto { +export class CreatePermissionDto { @IsUUID(4) @IsOptional() - pageId: string; + id: string; @IsEnum(PAGE_PERMISSION_TYPE) type: PAGE_PERMISSION_TYPE; diff --git a/server/src/modules/app-permissions/interfaces/IController.ts b/server/src/modules/app-permissions/interfaces/IController.ts index bfa35aa730..a7808d280d 100644 --- a/server/src/modules/app-permissions/interfaces/IController.ts +++ b/server/src/modules/app-permissions/interfaces/IController.ts @@ -1,6 +1,6 @@ import { User } from '@entities/user.entity'; import { Response } from 'express'; -import { CreatePagePermissionDto } from '../dto'; +import { CreatePermissionDto } from '../dto'; export interface IAppPermissionsController { fetchUsers(user: User, appId: string, response: Response): Promise; @@ -13,7 +13,7 @@ export interface IAppPermissionsController { user: User, appId: string, pageId: string, - body: CreatePagePermissionDto, + body: CreatePermissionDto, response: Response ): Promise; @@ -21,9 +21,29 @@ export interface IAppPermissionsController { user: User, appId: string, pageId: string, - body: CreatePagePermissionDto, + body: CreatePermissionDto, response: Response ): Promise; deletePagePermissions(user: User, appId: string, pageId: string, response: Response): Promise; + + fetchQueryPermissions(user: User, appId: string, queryId: string, response: Response): Promise; + + createQueryPermissions( + user: User, + appId: string, + queryId: string, + body: CreatePermissionDto, + response: Response + ): Promise; + + updateQueryPermissions( + user: User, + appId: string, + queryId: string, + body: CreatePermissionDto, + response: Response + ): Promise; + + deleteQueryPermissions(user: User, appId: string, queryId: string, response: Response): Promise; } diff --git a/server/src/modules/app-permissions/interfaces/IService.ts b/server/src/modules/app-permissions/interfaces/IService.ts index cad5fef726..aa1892da43 100644 --- a/server/src/modules/app-permissions/interfaces/IService.ts +++ b/server/src/modules/app-permissions/interfaces/IService.ts @@ -1,16 +1,23 @@ import { User } from '@entities/user.entity'; -import { CreatePagePermissionDto } from '../dto'; +import { CreatePermissionDto } from '../dto'; +import { PERMISSION_ENTITY_TYPE } from '../constants'; export interface IAppPermissionsService { fetchUsers(appId: string, user: User): Promise; fetchUserGroups(appId: string, user: User): Promise; - fetchPagePermissions(pageId: string): Promise; + fetchAppPermissions(type: PERMISSION_ENTITY_TYPE, id: string): Promise; - createPagePermissions(pageId: string, body: CreatePagePermissionDto): Promise; + createAppPermissions(type: PERMISSION_ENTITY_TYPE, id: string, body: CreatePermissionDto): Promise; - updatePagePermissions(appId: string, pageId: string, body: CreatePagePermissionDto, user: User): Promise; + updateAppPermissions( + type: PERMISSION_ENTITY_TYPE, + appId: string, + id: string, + body: CreatePermissionDto, + user: User + ): Promise; - deletePagePermissions(pageId: string): Promise; + deleteAppPermissions(type: PERMISSION_ENTITY_TYPE, id: string): Promise; } diff --git a/server/src/modules/app-permissions/interfaces/IUtilService.ts b/server/src/modules/app-permissions/interfaces/IUtilService.ts index 06654ed9e9..dbd390982a 100644 --- a/server/src/modules/app-permissions/interfaces/IUtilService.ts +++ b/server/src/modules/app-permissions/interfaces/IUtilService.ts @@ -1,13 +1,17 @@ import { User } from '@entities/user.entity'; import { GroupPermissions } from '@entities/group_permissions.entity'; -import { CreatePagePermissionDto } from '../dto'; +import { CreatePermissionDto } from '../dto'; export interface IUtilService { getUsersWithViewAccess(appId: string, organizationId: string): Promise; getUserGroupsWithViewAccess(appId: string, organizationId: string): Promise; - createPagePermission(pageId: string, body: CreatePagePermissionDto): Promise; + createPagePermission(pageId: string, body: CreatePermissionDto): Promise; - updatePagePermission(pageId: string, body: CreatePagePermissionDto): Promise; + updatePagePermission(pageId: string, body: CreatePermissionDto): Promise; + + createQueryPermission(queryId: string, body: CreatePermissionDto): Promise; + + updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise; } diff --git a/server/src/modules/app-permissions/module.ts b/server/src/modules/app-permissions/module.ts index 704c6c1374..5e3e3db107 100644 --- a/server/src/modules/app-permissions/module.ts +++ b/server/src/modules/app-permissions/module.ts @@ -7,8 +7,12 @@ import { User } from '@entities/user.entity'; import { RolesRepository } from '@modules/roles/repository'; import { PageUsersRepository } from './repositories/page-users.repository'; import { PagePermissionsRepository } from './repositories/page-permissions.repository'; +import { QueryUsersRepository } from './repositories/query-users.repository'; +import { QueryPermissionsRepository } from './repositories/query-permissions.repository'; import { PageUser } from '@entities/page_users.entity'; import { PagePermission } from '@entities/page_permissions.entity'; +import { QueryUser } from '@entities/query_users.entity'; +import { QueryPermission } from '@entities/query_permissions.entity'; export class AppPermissionsModule { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { @@ -19,7 +23,9 @@ export class AppPermissionsModule { return { module: AppPermissionsModule, - imports: [TypeOrmModule.forFeature([GroupPermissions, User, PageUser, PagePermission])], + imports: [ + TypeOrmModule.forFeature([GroupPermissions, User, PageUser, PagePermission, QueryUser, QueryPermission]), + ], controllers: [AppPermissionsController], providers: [ AppPermissionsService, @@ -27,6 +33,8 @@ export class AppPermissionsModule { RolesRepository, PageUsersRepository, PagePermissionsRepository, + QueryUsersRepository, + QueryPermissionsRepository, FeatureAbilityFactory, ], exports: [AppPermissionsUtilService, AppPermissionsService], diff --git a/server/src/modules/app-permissions/repositories/query-permissions.repository.ts b/server/src/modules/app-permissions/repositories/query-permissions.repository.ts new file mode 100644 index 0000000000..d83976ca68 --- /dev/null +++ b/server/src/modules/app-permissions/repositories/query-permissions.repository.ts @@ -0,0 +1,58 @@ +import { QueryPermission } from '@entities/query_permissions.entity'; +import { Injectable } from '@nestjs/common'; +import { DataSource, EntityManager, Repository } from 'typeorm'; +import { QueryUsersRepository } from './query-users.repository'; +import { dbTransactionWrap } from '@helpers/database.helper'; +import { PAGE_PERMISSION_TYPE } from '../constants'; + +@Injectable() +export class QueryPermissionsRepository extends Repository { + constructor(private dataSource: DataSource, private readonly queryUsersRepository: QueryUsersRepository) { + super(QueryPermission, dataSource.createEntityManager()); + } + + async getQueryPermissions(queryId: string, manager?: EntityManager): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const queryPermissions = await manager.find(QueryPermission, { + where: { queryId }, + relations: ['users', 'users.user', 'users.permissionGroup'], + }); + + return queryPermissions.map((permission) => { + if (permission.type === PAGE_PERMISSION_TYPE.GROUP) { + return { + ...permission, + groups: permission.users, + users: undefined, + }; + } + return permission; + }); + }, manager || this.manager); + } + + async createQueryPermissions( + queryId: string, + type: PAGE_PERMISSION_TYPE, + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const existingPermission = await manager.findOne(QueryPermission, { where: { queryId } }); + if (existingPermission) { + throw new Error(`Query permission already exists for Query id: ${queryId}`); + } + + const queryPermission = manager.create(QueryPermission, { + queryId, + type, + }); + return manager.save(queryPermission); + }, manager || this.manager); + } + + async deleteQueryPermissions(queryId: string, manager?: EntityManager): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + await manager.delete(QueryPermission, { queryId }); + }, manager || this.manager); + } +} diff --git a/server/src/modules/app-permissions/repositories/query-users.repository.ts b/server/src/modules/app-permissions/repositories/query-users.repository.ts new file mode 100644 index 0000000000..b5a2cc08fa --- /dev/null +++ b/server/src/modules/app-permissions/repositories/query-users.repository.ts @@ -0,0 +1,83 @@ +import { QueryUser } from '@entities/query_users.entity'; +import { Injectable } from '@nestjs/common'; +import { DataSource, EntityManager, Repository } from 'typeorm'; +import { dbTransactionWrap } from '@helpers/database.helper'; +import { QueryPermission } from '@entities/query_permissions.entity'; + +@Injectable() +export class QueryUsersRepository extends Repository { + constructor(private dataSource: DataSource) { + super(QueryUser, dataSource.createEntityManager()); + } + + async createQueryUsersWithSingle( + queryPermissionsId: string, + users: string[], + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const queryUsers = users.map((userId) => { + return manager.create(QueryUser, { + queryPermissionsId, + userId, + permissionGroupsId: null, + }); + }); + return manager.save(queryUsers); + }, manager || this.manager); + } + + async createQueryUsersWithGroup( + queryPermissionsId: string, + groups: string[], + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const queryUsers = groups.map((permissionGroupsId) => { + return manager.create(QueryUser, { + queryPermissionsId, + permissionGroupsId, + userId: null, + }); + }); + return manager.save(queryUsers); + }, manager || this.manager); + } + + async checkQueryUserWithGroup( + queryPermission: QueryPermission, + userId: string, + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const result = await manager + .createQueryBuilder(QueryUser, 'query_users') + .innerJoin('query_users.permissionGroup', 'group') + .innerJoin('group.groupUsers', 'groupUser') + .where('query_users.queryPermission = :permissionId', { + permissionId: queryPermission.id, + }) + .andWhere('groupUser.userId = :userId', { userId }) + .getOne(); + + return !!result; + }, manager || this.manager); + } + + async checkQueryUserWithSingle( + queryPermission: QueryPermission, + userId: string, + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const queryUser = await manager.findOne(QueryUser, { + where: { + queryPermission: { id: queryPermission.id }, + userId, + }, + }); + + return !!queryUser; + }, manager || this.manager); + } +} diff --git a/server/src/modules/app-permissions/service.ts b/server/src/modules/app-permissions/service.ts index c6b5bc640d..0aed033079 100644 --- a/server/src/modules/app-permissions/service.ts +++ b/server/src/modules/app-permissions/service.ts @@ -13,19 +13,19 @@ export class AppPermissionsService implements IAppPermissionsService { throw new Error('Method not implemented.'); } - async fetchPagePermissions(pageId) { + async fetchAppPermissions(type, id) { throw new Error('Method not implemented.'); } - async createPagePermissions(pageId, body) { + async createAppPermissions(type, id, body) { throw new Error('Method not implemented.'); } - async updatePagePermissions(appId, pageId, body, user) { + async updateAppPermissions(type, appId, id, body, user) { throw new Error('Method not implemented.'); } - async deletePagePermissions(pageId) { + async deleteAppPermissions(type, id) { throw new Error('Method not implemented.'); } } diff --git a/server/src/modules/app-permissions/types/index.ts b/server/src/modules/app-permissions/types/index.ts index 86a41afba1..d377f5a08f 100644 --- a/server/src/modules/app-permissions/types/index.ts +++ b/server/src/modules/app-permissions/types/index.ts @@ -9,6 +9,10 @@ interface Features { [FEATURE_KEY.CREATE_PAGE_PERMISSIONS]: FeatureConfig; [FEATURE_KEY.UPDATE_PAGE_PERMISSIONS]: FeatureConfig; [FEATURE_KEY.DELETE_PAGE_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.FETCH_QUERY_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: FeatureConfig; } export interface FeaturesConfig { diff --git a/server/src/modules/app-permissions/util.service.ts b/server/src/modules/app-permissions/util.service.ts index 71432a0e4b..7ff894a058 100644 --- a/server/src/modules/app-permissions/util.service.ts +++ b/server/src/modules/app-permissions/util.service.ts @@ -2,7 +2,7 @@ import { User } from '@entities/user.entity'; import { IUtilService } from './interfaces/IUtilService'; import { Injectable } from '@nestjs/common'; import { GroupPermissions } from '@entities/group_permissions.entity'; -import { CreatePagePermissionDto } from './dto'; +import { CreatePermissionDto } from './dto'; @Injectable() export class AppPermissionsUtilService implements IUtilService { @@ -16,11 +16,19 @@ export class AppPermissionsUtilService implements IUtilService { throw new Error('Method not implemented.'); } - async createPagePermission(pageId: string, body: CreatePagePermissionDto): Promise { + async createPagePermission(pageId: string, body: CreatePermissionDto): Promise { throw new Error('Method not implemented.'); } - async updatePagePermission(pageId: string, body: CreatePagePermissionDto): Promise { + async updatePagePermission(pageId: string, body: CreatePermissionDto): Promise { + throw new Error('Method not implemented.'); + } + + async createQueryPermission(queryId: string, body: CreatePermissionDto): Promise { + throw new Error('Method not implemented.'); + } + + async updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise { throw new Error('Method not implemented.'); } } 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 ccee419f3a..7a6a089548 100644 --- a/server/src/modules/apps/services/app-import-export.service.ts +++ b/server/src/modules/apps/services/app-import-export.service.ts @@ -39,6 +39,8 @@ 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'; +import { QueryPermission } from '@entities/query_permissions.entity'; +import { QueryUser } from '@entities/query_users.entity'; interface AppResourceMappings { defaultDataSourceIdMapping: Record; dataQueryMapping: Record; @@ -161,6 +163,9 @@ export class AppImportExportService { if (dataSources?.length) { dataQueries = await manager .createQueryBuilder(DataQuery, 'data_queries') + .leftJoinAndSelect('data_queries.permissions', 'permission') + .leftJoinAndSelect('permission.users', 'queryUser') + .leftJoinAndSelect('queryUser.permissionGroup', 'permissionGroup') .where('data_queries.dataSourceId IN(:...dataSourceId)', { dataSourceId: dataSources?.map((v) => v.id), }) @@ -213,6 +218,21 @@ export class AppImportExportService { }; }); + const queriesWithPermissionGroups = dataQueries.map((query) => { + const groupPermission = query.permissions.find((perm) => perm.type === 'GROUP'); + + return { + ...query, + 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 @@ -236,7 +256,7 @@ export class AppImportExportService { appToExport['components'] = components; appToExport['pages'] = pagesWithPermissionGroups; appToExport['events'] = events; - appToExport['dataQueries'] = dataQueries; + appToExport['dataQueries'] = queriesWithPermissionGroups; appToExport['dataSources'] = dataSources; appToExport['appVersions'] = appVersions; appToExport['appEnvironments'] = appEnvironments; @@ -1119,7 +1139,15 @@ export class AppImportExportService { }); await manager.save(newQuery); + + if (importingQuery.permissions) { + newQuery.permissions = importingQuery.permissions; + } + appResourceMappings.dataQueryMapping[importingQuery.id] = newQuery.id; + + //create query permissions of query if flag enabled in dto + await this.createQueryPermissionsForGroups(newQuery, organizationId, manager); } return appResourceMappings; @@ -1355,7 +1383,7 @@ export class AppImportExportService { return pageSettings; } - async checkIfGroupPermissionsExist(pages, organizationId) { + async checkIfGroupPermissionsExist(pages, queries, organizationId) { const allGroupNames = new Set(); for (const page of pages) { @@ -1365,6 +1393,15 @@ export class AppImportExportService { } } + for (const query of queries) { + const groupNames = query.permissions?.permissionGroup || []; + for (const name of groupNames) { + if (!allGroupNames.has(name)) { + allGroupNames.add(name); + } + } + } + if (!allGroupNames.size) return; return await dbTransactionWrap(async (manager: EntityManager) => { @@ -1425,6 +1462,41 @@ export class AppImportExportService { await manager.save(pageUsers); } + async createQueryPermissionsForGroups(query, organizationId: string, manager: EntityManager) { + const groupNames = query.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(QueryPermission, { + queryId: query.id, + type: PAGE_PERMISSION_TYPE.GROUP, + }); + + const savedPermission = await manager.save(permission); + + const queryUsers = validGroupNames.map((name) => + manager.create(QueryUser, { + queryPermissionsId: savedPermission.id, + permissionGroupsId: groupMap.get(name).id, + }) + ); + + await manager.save(queryUsers); + } + async createAppVersionsForImportedApp( manager: EntityManager, user: User, diff --git a/server/src/modules/data-queries/controller.ts b/server/src/modules/data-queries/controller.ts index 831834ef8a..e734810703 100644 --- a/server/src/modules/data-queries/controller.ts +++ b/server/src/modules/data-queries/controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, Body, Post, Patch, Delete, UseGuards, Put, Res } from '@nestjs/common'; +import { Controller, Get, Param, Body, Post, Patch, Delete, UseGuards, Put, Res, Query } from '@nestjs/common'; import { JwtAuthGuard } from '@modules/session/guards/jwt-auth.guard'; import { DataQueriesService } from './service'; import { User, UserEntity } from '@modules/app/decorators/user.decorator'; @@ -30,8 +30,8 @@ export class DataQueriesController implements IDataQueriesController { @InitFeature(FEATURE_KEY.GET) @UseGuards(JwtAuthGuard, ValidateAppVersionGuard, ValidateQueryAppGuard, AppFeatureAbilityGuard) @Get(':versionId') - index(@Param('versionId') versionId: string) { - return this.dataQueriesService.getAll(versionId); + index(@User() user: UserEntity, @Param('versionId') versionId: string, @Query('mode') mode?: string) { + return this.dataQueriesService.getAll(user, versionId, mode); } @InitFeature(FEATURE_KEY.CREATE) @@ -113,7 +113,8 @@ export class DataQueriesController implements IDataQueriesController { @Body() updateDataQueryDto: UpdateDataQueryDto, @Ability() ability: AppAbility, @DataSource() dataSource: DataSourceEntity, - @Res({ passthrough: true }) response: Response + @Res({ passthrough: true }) response: Response, + @Query('mode') mode?: string ) { return this.dataQueriesService.runQueryOnBuilder( user, @@ -122,7 +123,8 @@ export class DataQueriesController implements IDataQueriesController { updateDataQueryDto, ability, dataSource, - response + response, + mode ); } diff --git a/server/src/modules/data-queries/interfaces/IController.ts b/server/src/modules/data-queries/interfaces/IController.ts index d68f28b382..59292772e2 100644 --- a/server/src/modules/data-queries/interfaces/IController.ts +++ b/server/src/modules/data-queries/interfaces/IController.ts @@ -7,7 +7,7 @@ import { App } from '@entities/app.entity'; import { UpdateSourceDto } from '../dto'; import { Response } from 'express'; export interface IDataQueriesController { - index(versionId: string): Promise; + index(user: UserEntity, versionId: string, mode?: string): Promise; create( user: UserEntity, @@ -36,7 +36,8 @@ export interface IDataQueriesController { updateDataQueryDto: UpdateDataQueryDto, ability: AppAbility, dataSource: DataSourceEntity, - response: Response + response: Response, + mode?: string ): Promise; runQuery( diff --git a/server/src/modules/data-queries/interfaces/IService.ts b/server/src/modules/data-queries/interfaces/IService.ts index 14848e6a45..08127d22ce 100644 --- a/server/src/modules/data-queries/interfaces/IService.ts +++ b/server/src/modules/data-queries/interfaces/IService.ts @@ -5,7 +5,7 @@ import { CreateDataQueryDto, IUpdatingReferencesOptions, UpdateDataQueryDto } fr import { DataQuery } from '@entities/data_query.entity'; export interface IDataQueriesService { - getAll(versionId: string): Promise<{ data_queries: object[] }>; + getAll(user: User, versionId: string, mode?: string): Promise<{ data_queries: object[] }>; create(user: User, dataSource: DataSource, dataQueryDto: CreateDataQueryDto): Promise; @@ -22,7 +22,8 @@ export interface IDataQueriesService { updateDataQueryDto: UpdateDataQueryDto, ability: object, dataSource: DataSource, - response: Response + response: Response, + mode?: string ): Promise; runQueryForApp( diff --git a/server/src/modules/data-queries/interfaces/IUtilService.ts b/server/src/modules/data-queries/interfaces/IUtilService.ts index f070380b54..3b5bdd10f1 100644 --- a/server/src/modules/data-queries/interfaces/IUtilService.ts +++ b/server/src/modules/data-queries/interfaces/IUtilService.ts @@ -14,7 +14,8 @@ export interface IDataQueriesUtilService { dataQuery: any, queryOptions: object, response: Response, - environmentId?: string + environmentId?: string, + mode?: string ): Promise; fetchServiceAndParsedParams( diff --git a/server/src/modules/data-queries/module.ts b/server/src/modules/data-queries/module.ts index 5ec6b2c1a8..11f15d3994 100644 --- a/server/src/modules/data-queries/module.ts +++ b/server/src/modules/data-queries/module.ts @@ -9,6 +9,8 @@ import { FeatureAbilityFactory as AppFeatureAbilityFactory } from './ability/app import { FeatureAbilityFactory as DataSourceFeatureAbilityFactory } from './ability/data-source'; import { AppsRepository } from '@modules/apps/repository'; import { OrganizationRepository } from '@modules/organizations/repository'; +import { LicenseModule } from '@modules/licensing/module'; +import { AppPermissionsModule } from '@modules/app-permissions/module'; export class DataQueriesModule { static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { @@ -19,7 +21,12 @@ export class DataQueriesModule { return { module: DataQueriesModule, - imports: [await AppEnvironmentsModule.register(configs), await DataSourcesModule.register(configs)], + imports: [ + await AppEnvironmentsModule.register(configs), + await DataSourcesModule.register(configs), + await LicenseModule.forRoot(configs), + await AppPermissionsModule.register(configs), + ], providers: [ DataQueryRepository, VersionRepository, diff --git a/server/src/modules/data-queries/repository.ts b/server/src/modules/data-queries/repository.ts index 8caf25ed1f..7ba62a974a 100644 --- a/server/src/modules/data-queries/repository.ts +++ b/server/src/modules/data-queries/repository.ts @@ -50,6 +50,25 @@ export class DataQueryRepository extends Repository { }); } + getAllWithPermissions(appVersionId: string): Promise { + return dbTransactionWrap((manager: EntityManager) => { + return manager + .createQueryBuilder(DataQuery, 'data_query') + .innerJoinAndSelect('data_query.dataSource', 'data_source') + .leftJoinAndSelect('data_query.plugins', 'plugins') + .leftJoinAndSelect('plugins.iconFile', 'iconFile') + .leftJoinAndSelect('plugins.manifestFile', 'manifestFile') + .leftJoinAndSelect('data_query.permissions', 'permission') + .leftJoinAndSelect('permission.users', 'queryUser') + .leftJoinAndSelect('queryUser.user', 'user') + .leftJoinAndSelect('queryUser.permissionGroup', 'group') + .where('data_source.appVersionId = :appVersionId', { appVersionId }) + .where('data_query.app_version_id = :appVersionId', { appVersionId }) + .orderBy('data_query.updatedAt', 'DESC') + .getMany(); + }); + } + async createOne(data: Partial, manager?: EntityManager): Promise { return dbTransactionWrap((manager: EntityManager) => { const newDataQuery = manager.create(DataQuery, { diff --git a/server/src/modules/data-queries/service.ts b/server/src/modules/data-queries/service.ts index 5337c6739a..fc71a93b6e 100644 --- a/server/src/modules/data-queries/service.ts +++ b/server/src/modules/data-queries/service.ts @@ -24,7 +24,7 @@ export class DataQueriesService implements IDataQueriesService { protected readonly dataSourceRepository: DataSourcesRepository ) { } - async getAll(versionId: string) { + async getAll(user: User, versionId: string, mode?: string) { const queries = await this.dataQueryRepository.getAll(versionId); const serializedQueries = []; @@ -127,7 +127,8 @@ export class DataQueriesService implements IDataQueriesService { updateDataQueryDto: UpdateDataQueryDto, ability: AppAbility, dataSource: DataSource, - response: Response + response: Response, + mode?: string ) { const { options, resolvedOptions } = updateDataQueryDto; @@ -138,7 +139,7 @@ export class DataQueriesService implements IDataQueriesService { dataQuery['options'] = options; } - return this.runAndGetResult(user, dataQuery, resolvedOptions, response, environmentId); + return this.runAndGetResult(user, dataQuery, resolvedOptions, response, environmentId, mode); } async runQueryForApp(user: User, dataQueryId: string, updateDataQueryDto: UpdateDataQueryDto, response: Response) { @@ -153,17 +154,25 @@ export class DataQueriesService implements IDataQueriesService { return this.runAndGetResult(user, dataQuery, options, response, environmentId); } - private async runAndGetResult( + protected async runAndGetResult( user: User, dataQuery: DataQuery, resolvedOptions: object, response: Response, - environmentId?: string + environmentId?: string, + mode?: string ): Promise { let result = {}; try { - result = await this.dataQueryUtilService.runQuery(user, dataQuery, resolvedOptions, response, environmentId); + result = await this.dataQueryUtilService.runQuery( + user, + dataQuery, + resolvedOptions, + response, + environmentId, + mode + ); } catch (error) { if (error.constructor.name === 'QueryError') { result = { diff --git a/server/src/modules/data-queries/util.service.ts b/server/src/modules/data-queries/util.service.ts index 6068c62bcc..1478e2a699 100644 --- a/server/src/modules/data-queries/util.service.ts +++ b/server/src/modules/data-queries/util.service.ts @@ -63,7 +63,8 @@ export class DataQueriesUtilService implements IDataQueriesUtilService { dataQuery: any, queryOptions: object, response: Response, - environmentId?: string + environmentId?: string, + mode?: string ): Promise { let result; const queryStatus = new DataQueryStatus(); @@ -320,7 +321,7 @@ export class DataQueriesUtilService implements IDataQueriesUtilService { return { service, sourceOptions, parsedQueryOptions }; } - private getCurrentUserToken = (isMultiAuthEnabled: boolean, tokenData: any, userId: string, isAppPublic: boolean) => { + protected getCurrentUserToken = (isMultiAuthEnabled: boolean, tokenData: any, userId: string, isAppPublic: boolean) => { if (isMultiAuthEnabled) { if (!tokenData || !Array.isArray(tokenData)) return null; return !isAppPublic diff --git a/server/src/modules/import-export-resources/service.ts b/server/src/modules/import-export-resources/service.ts index b03697397b..465524076a 100644 --- a/server/src/modules/import-export-resources/service.ts +++ b/server/src/modules/import-export-resources/service.ts @@ -70,15 +70,17 @@ export class ImportExportResourcesService { let tableNameMapping = {}; const imports = { app: [], tooljet_database: [], tableNameMapping: {} }; const importingVersion = importResourcesDto.tooljet_version; - const skipPagePermissionsGroupCheck = importResourcesDto.skip_page_permissions_group_check; + const skipPermissionsGroupCheck = importResourcesDto.skip_permissions_group_check; - if (!isEmpty(importResourcesDto.app) && !skipPagePermissionsGroupCheck) { + if (!isEmpty(importResourcesDto.app) && !skipPermissionsGroupCheck) { 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)); + const queries = appParams?.dataQueries; + (pages?.length || queries?.length) && + (await this.appImportExportService.checkIfGroupPermissionsExist(pages, queries, user.organizationId)); } } } diff --git a/server/src/modules/versions/repository.ts b/server/src/modules/versions/repository.ts index 7bc12a4e19..9f3dd5f017 100644 --- a/server/src/modules/versions/repository.ts +++ b/server/src/modules/versions/repository.ts @@ -107,6 +107,21 @@ export class VersionRepository extends Repository { }, manager || this.manager); } + async findDataQueriesForVersionWithPermissions(appVersionId: string, manager?: EntityManager): Promise { + return dbTransactionWrap((manager: EntityManager) => { + return manager + .createQueryBuilder(DataQuery, 'query') + .where('query.appVersionId = :appVersionId', { appVersionId }) + .leftJoinAndSelect('query.dataSource', 'dataSource') + .leftJoinAndSelect('query.permissions', 'permission') + .leftJoinAndSelect('permission.users', 'queryUser') + .leftJoinAndSelect('queryUser.user', 'user') + .leftJoinAndSelect('queryUser.permissionGroup', 'group') + .select(['query', 'dataSource.kind', 'permission', 'queryUser', 'user', 'group']) + .getMany(); + }, manager || this.manager); + } + async findVersion(id: string, manager?: EntityManager): Promise { return await dbTransactionWrap(async (manager: EntityManager) => { const appVersion = await manager.findOneOrFail(AppVersion, {