mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 00:48:25 +00:00
Merge pull request #12960 from ToolJet/feat/component-permissions
Feat: Component level permissions
This commit is contained in:
commit
a0f2115c30
38 changed files with 810 additions and 63 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 9458c8d66f29f8334765b5757dd096139a8d53d2
|
||||
Subproject commit 51d0a7fbe974919786c938304e2214d46396c033
|
||||
|
|
@ -4,6 +4,7 @@ import './configHandle.scss';
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { findHighestLevelofSelection } from '../Grid/gridUtils';
|
||||
import SolidIcon from '@/_ui/Icon/solidIcons/index';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { DROPPABLE_PARENTS } from '../appCanvasConstants';
|
||||
|
||||
|
|
@ -52,7 +53,40 @@ export const ConfigHandle = ({
|
|||
);
|
||||
}, shallow);
|
||||
|
||||
const currentPageIndex = useStore((state) => state.modules.canvas.currentPageIndex);
|
||||
const component = useStore((state) => state.modules.canvas.pages[currentPageIndex].components[id]);
|
||||
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
|
||||
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
|
||||
const isRestricted = component.permissions && component.permissions.length !== 0;
|
||||
const draggingComponentId = useStore((state) => state.draggingComponentId);
|
||||
|
||||
let height = visibility === false ? 10 : widgetHeight;
|
||||
|
||||
const getTooltip = () => {
|
||||
const permission = component.permissions?.[0];
|
||||
if (!permission) return null;
|
||||
|
||||
const users = permission.groups || permission.users || [];
|
||||
if (users.length === 0) return null;
|
||||
|
||||
const isSingle = permission.type === 'SINGLE';
|
||||
const isGroup = permission.type === 'GROUP';
|
||||
|
||||
if (isSingle) {
|
||||
return users.length === 1
|
||||
? `Access restricted to ${users[0].user.email}`
|
||||
: `Access restricted to ${users.length} users`;
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
return users.length === 1
|
||||
? `Access restricted to ${users[0].permission_group?.name || users[0].permissionGroup?.name} group`
|
||||
: `Access restricted to ${users.length} user groups`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`config-handle ${customClassName}`}
|
||||
|
|
@ -78,6 +112,22 @@ export const ConfigHandle = ({
|
|||
}
|
||||
}}
|
||||
>
|
||||
{licenseValid && isRestricted && (
|
||||
<ToolTip message={getTooltip()} show={licenseValid && isRestricted && !draggingComponentId}>
|
||||
<span
|
||||
style={{
|
||||
background:
|
||||
visibility === false ? '#c6cad0' : componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
|
||||
border: position === 'bottom' ? '1px solid white' : 'none',
|
||||
color: visibility === false && 'var(--text-placeholder)',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
className="badge handle-content"
|
||||
>
|
||||
<SolidIcon width="12" name="lock" fill="var(--icon-on-solid)" />
|
||||
</span>
|
||||
</ToolTip>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
background:
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ const CreateVersionModal = ({
|
|||
appId,
|
||||
setCurrentVersionId,
|
||||
selectedVersion,
|
||||
currentMode,
|
||||
} = useStore(
|
||||
(state) => ({
|
||||
createNewVersionAction: state.createNewVersionAction,
|
||||
|
|
@ -45,6 +46,7 @@ const CreateVersionModal = ({
|
|||
currentVersionId: state.currentVersionId,
|
||||
setCurrentVersionId: state.setCurrentVersionId,
|
||||
selectedVersion: state.selectedVersion,
|
||||
currentMode: state.currentMode,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
|
@ -94,7 +96,7 @@ const CreateVersionModal = ({
|
|||
setIsCreatingVersion(false);
|
||||
setShowCreateAppVersion(false);
|
||||
appVersionService
|
||||
.getAppVersionData(appId, newVersion.id)
|
||||
.getAppVersionData(appId, newVersion.id, currentMode)
|
||||
.then((data) => {
|
||||
setCurrentVersionId(newVersion.id);
|
||||
handleCommitOnVersionCreation(data);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ import useStore from '@/AppBuilder/_stores/store';
|
|||
import { componentTypes } from '@/AppBuilder/WidgetManager/componentTypes';
|
||||
import { copyComponents } from '@/AppBuilder/AppCanvas/appCanvasUtils.js';
|
||||
import DatetimePickerV2 from './Components/DatetimePickerV2.jsx';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal';
|
||||
import { appPermissionService } from '@/_services';
|
||||
import { ModuleContainerInspector, ModuleViewerInspector, ModuleEditorBanner } from '@/modules/Modules/components';
|
||||
|
||||
const INSPECTOR_HEADER_OPTIONS = [
|
||||
|
|
@ -61,6 +64,19 @@ const INSPECTOR_HEADER_OPTIONS = [
|
|||
value: 'duplicate',
|
||||
icon: <Copy width={16} />,
|
||||
},
|
||||
{
|
||||
label: 'Component permission',
|
||||
value: 'permission',
|
||||
icon: (
|
||||
<img
|
||||
alt="permission-icon"
|
||||
src="assets/images/icons/editor/left-sidebar/authorization.svg"
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
),
|
||||
trailingIcon: <SolidIcon width={16} name="enterprisecrown" className="mx-1" />,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
value: 'delete',
|
||||
|
|
@ -104,6 +120,11 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
|
|||
const isVersionReleased = useStore((state) => state.isVersionReleased);
|
||||
const setWidgetDeleteConfirmation = useStore((state) => state.setWidgetDeleteConfirmation);
|
||||
const setComponentToInspect = useStore((state) => state.setComponentToInspect);
|
||||
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
|
||||
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
|
||||
const showComponentPermissionModal = useStore((state) => state.showComponentPermissionModal);
|
||||
const toggleComponentPermissionModal = useStore((state) => state.toggleComponentPermissionModal);
|
||||
const setComponentPermission = useStore((state) => state.setComponentPermission);
|
||||
const dataQueries = useDataQueries();
|
||||
|
||||
const currentState = useCurrentState();
|
||||
|
|
@ -378,9 +399,14 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
|
|||
if (value === 'delete') {
|
||||
setWidgetDeleteConfirmation(true);
|
||||
}
|
||||
if (value === 'permission') {
|
||||
if (!licenseValid) return;
|
||||
toggleComponentPermissionModal(true);
|
||||
}
|
||||
if (value === 'duplicate') {
|
||||
copyComponents({ isCloning: true });
|
||||
}
|
||||
setShowHeaderActionsMenu(false);
|
||||
};
|
||||
const buildGeneralStyle = () => {
|
||||
if (!componentMeta?.definition?.generalStyles) {
|
||||
|
|
@ -446,7 +472,7 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
|
|||
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (showHeaderActionsMenu && event.target.closest('.list-menu') === null) {
|
||||
if (showHeaderActionsMenu && event.target.closest('#list-menu') === null) {
|
||||
setShowHeaderActionsMenu(false);
|
||||
}
|
||||
};
|
||||
|
|
@ -504,44 +530,79 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
|
|||
</div>
|
||||
<div className={`col-9 p-0 ${shouldFreeze && 'disabled'}`}>{renderAppNameInput()}</div>
|
||||
{!isModuleContainer && (
|
||||
<div className="col-2" data-cy={'component-inspector-options'}>
|
||||
<OverlayTrigger
|
||||
trigger={'click'}
|
||||
placement={'bottom-end'}
|
||||
rootClose={false}
|
||||
show={showHeaderActionsMenu}
|
||||
overlay={
|
||||
<Popover id="list-menu" className={darkMode && 'dark-theme'}>
|
||||
<Popover.Body bsPrefix="list-item-popover-body">
|
||||
{INSPECTOR_HEADER_OPTIONS.map((option) => (
|
||||
<div
|
||||
data-cy={`component-inspector-${String(option?.value).toLowerCase()}-button`}
|
||||
className="list-item-popover-option"
|
||||
key={option?.value}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleInspectorHeaderActions(option.value);
|
||||
}}
|
||||
>
|
||||
<div className="list-item-popover-menu-option-icon">{option.icon}</div>
|
||||
<div
|
||||
className={classNames('list-item-option-menu-label', {
|
||||
'color-tomato9': option.value === 'delete',
|
||||
})}
|
||||
>
|
||||
{option?.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<span className="cursor-pointer" onClick={() => setShowHeaderActionsMenu(true)}>
|
||||
<SolidIcon data-cy={'menu-icon'} name="morevertical" width="24" fill={'var(--slate12)'} />
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
<>
|
||||
<div className="col-2" data-cy={'component-inspector-options'}>
|
||||
<OverlayTrigger
|
||||
trigger={'click'}
|
||||
placement={'bottom-end'}
|
||||
rootClose={false}
|
||||
show={showHeaderActionsMenu}
|
||||
overlay={
|
||||
<Popover id="list-menu" className={darkMode && 'dark-theme'}>
|
||||
<Popover.Body bsPrefix="list-item-popover-body">
|
||||
{INSPECTOR_HEADER_OPTIONS.map((option) => {
|
||||
const optionBody = (
|
||||
<div
|
||||
data-cy={`component-inspector-${String(option?.value).toLowerCase()}-button`}
|
||||
className="list-item-popover-option"
|
||||
key={option?.value}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleInspectorHeaderActions(option.value);
|
||||
}}
|
||||
>
|
||||
<div className="list-item-popover-menu-option-icon">{option.icon}</div>
|
||||
<div
|
||||
className={classNames('list-item-option-menu-label', {
|
||||
'color-tomato9': option.value === 'delete',
|
||||
'color-disabled': option.value === 'permission' && !licenseValid,
|
||||
})}
|
||||
>
|
||||
{option?.label}
|
||||
</div>
|
||||
{option.value === 'permission' &&
|
||||
!licenseValid &&
|
||||
option.trailingIcon &&
|
||||
option.trailingIcon}
|
||||
</div>
|
||||
);
|
||||
|
||||
return option.value === 'permission' ? (
|
||||
<ToolTip
|
||||
key={option.value}
|
||||
message={'Component permissions are available only in paid plans'}
|
||||
placement="left"
|
||||
show={!licenseValid}
|
||||
>
|
||||
{optionBody}
|
||||
</ToolTip>
|
||||
) : (
|
||||
optionBody
|
||||
);
|
||||
})}
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<span className="cursor-pointer" onClick={() => setShowHeaderActionsMenu(true)}>
|
||||
<SolidIcon data-cy={'menu-icon'} name="morevertical" width="24" fill={'var(--slate12)'} />
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
<AppPermissionsModal
|
||||
modalType="component"
|
||||
resourceId={selectedComponentId}
|
||||
resourceName={allComponents[selectedComponentId]?.component?.name}
|
||||
showModal={showComponentPermissionModal}
|
||||
toggleModal={toggleComponentPermissionModal}
|
||||
darkMode={darkMode}
|
||||
fetchPermission={(id, appId) => appPermissionService.getComponentPermission(appId, id)}
|
||||
createPermission={(id, appId, body) => appPermissionService.createComponentPermission(appId, id, body)}
|
||||
updatePermission={(id, appId, body) => appPermissionService.updateComponentPermission(appId, id, body)}
|
||||
deletePermission={(id, appId) => appPermissionService.deleteComponentPermission(appId, id)}
|
||||
onSuccess={(data) => setComponentPermission(selectedComponentId, data)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -557,8 +618,8 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
|
|||
componentMeta.displayName === 'Toggle Switch (Legacy)'
|
||||
? 'Toggle (Legacy)'
|
||||
: componentMeta.displayName === 'Toggle Switch'
|
||||
? 'Toggle Switch'
|
||||
: componentMeta.component,
|
||||
? 'Toggle Switch'
|
||||
: componentMeta.component,
|
||||
})}
|
||||
</small>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ const useAppData = (
|
|||
appDataPromise = appService.fetchAppBySlug(slug);
|
||||
} else {
|
||||
appDataPromise = isPreviewForVersion
|
||||
? appVersionService.getAppVersionData(appId, versionId)
|
||||
? appVersionService.getAppVersionData(appId, versionId, mode)
|
||||
: appService.fetchApp(appId);
|
||||
}
|
||||
}
|
||||
|
|
@ -573,7 +573,7 @@ const useAppData = (
|
|||
if (isEnvChanged) {
|
||||
setEnvironmentLoadingState('loading');
|
||||
}
|
||||
appVersionService.getAppVersionData(appId, selectedVersion?.id).then(async (appData) => {
|
||||
appVersionService.getAppVersionData(appId, selectedVersion?.id, mode).then(async (appData) => {
|
||||
cleanUpStore(false);
|
||||
const { should_freeze_editor } = appData;
|
||||
setIsEditorFreezed(should_freeze_editor);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ const initialState = {
|
|||
showWidgetDeleteConfirmation: false,
|
||||
focusedParentId: null,
|
||||
modalsOpenOnCanvas: [],
|
||||
showComponentPermissionModal: false,
|
||||
};
|
||||
|
||||
export const createComponentsSlice = (set, get) => ({
|
||||
|
|
@ -1989,4 +1990,25 @@ export const createComponentsSlice = (set, get) => ({
|
|||
|
||||
setComponentProperty(componentId, `canvasHeight`, maxHeight, 'properties', 'value', false);
|
||||
},
|
||||
toggleComponentPermissionModal: (show) => {
|
||||
set((state) => {
|
||||
state.showComponentPermissionModal = show;
|
||||
});
|
||||
},
|
||||
setComponentPermission: (componentId, data) => {
|
||||
const { modules } = get();
|
||||
const currentPageIndex = modules.canvas.currentPageIndex;
|
||||
const component = modules.canvas.pages[currentPageIndex]?.components?.[componentId];
|
||||
|
||||
if (component) {
|
||||
const updatedComponent = {
|
||||
...component,
|
||||
permissions: data.length === 0 || data.length === undefined ? [] : [data[0]],
|
||||
};
|
||||
|
||||
set((state) => {
|
||||
state.modules.canvas.pages[currentPageIndex].components[componentId] = updatedComponent;
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
|
|||
},
|
||||
changeEditorVersionAction: async (appId, versionId, onSuccess, onFailure) => {
|
||||
try {
|
||||
const data = await appVersionService.getAppVersionData(appId, versionId);
|
||||
const data = await appVersionService.getAppVersionData(appId, versionId, get().currentMode);
|
||||
const selectedVersion = {
|
||||
id: data.editing_version.id,
|
||||
name: data.editing_version.name,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ export const appPermissionService = {
|
|||
createQueryPermission,
|
||||
updateQueryPermission,
|
||||
deleteQueryPermission,
|
||||
getComponentPermission,
|
||||
createComponentPermission,
|
||||
updateComponentPermission,
|
||||
deleteComponentPermission,
|
||||
};
|
||||
|
||||
function getPagePermission(appId, pageId) {
|
||||
|
|
@ -89,3 +93,49 @@ function deleteQueryPermission(appId, queryId) {
|
|||
};
|
||||
return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getComponentPermission(appId, componentId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function createComponentPermission(appId, componentId, body) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function updateComponentPermission(appId, componentId, body) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function deleteComponentPermission(appId, componentId) {
|
||||
const requestOptions = {
|
||||
method: 'DELETE',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ function promoteEnvironment(appId, versionId, currentEnvironmentId) {
|
|||
};
|
||||
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/promote`, requestOptions).then(handleResponse);
|
||||
}
|
||||
function getAppVersionData(appId, versionId) {
|
||||
function getAppVersionData(appId, versionId, mode) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse);
|
||||
return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}?mode=${mode}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function create(appId, versionName, versionFromId, currentEnvironmentId) {
|
||||
|
|
|
|||
|
|
@ -2800,7 +2800,7 @@ hr {
|
|||
}
|
||||
|
||||
.config-handle {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.apps-table {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit e76477d30eb21df5d188ca204c520df75fddd529
|
||||
Subproject commit 213bba98018d82fe2fee0689e5b7bf1a19a85ade
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||
|
||||
export class CreateComponentPermissions1748509644056 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'component_permissions',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isGenerated: true,
|
||||
default: 'gen_random_uuid()',
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: 'component_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(
|
||||
'component_permissions',
|
||||
new TableForeignKey({
|
||||
columnNames: ['component_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'components',
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('component_permissions');
|
||||
}
|
||||
}
|
||||
76
server/migrations/1748509665915-CreateComponentUsers.ts
Normal file
76
server/migrations/1748509665915-CreateComponentUsers.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||
|
||||
export class CreateComponentUsers1748509665915 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'component_users',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isGenerated: true,
|
||||
default: 'gen_random_uuid()',
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: 'component_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(
|
||||
'component_users',
|
||||
new TableForeignKey({
|
||||
columnNames: ['component_permissions_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'component_permissions',
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
'component_users',
|
||||
new TableForeignKey({
|
||||
columnNames: ['user_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'users',
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
'component_users',
|
||||
new TableForeignKey({
|
||||
columnNames: ['permission_groups_id'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'permission_groups',
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('component_users');
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from 'typeorm';
|
||||
import { Page } from './page.entity';
|
||||
import { Layout } from './layout.entity';
|
||||
import { ComponentPermission } from './component_permissions.entity';
|
||||
|
||||
@Entity({ name: 'components' })
|
||||
@Index('idx_component_page_id', ['pageId'])
|
||||
|
|
@ -60,4 +61,7 @@ export class Component {
|
|||
|
||||
@OneToMany(() => Layout, (layout) => layout.component)
|
||||
layouts: Layout[];
|
||||
|
||||
@OneToMany(() => ComponentPermission, (permission) => permission.component)
|
||||
permissions: ComponentPermission[];
|
||||
}
|
||||
|
|
|
|||
29
server/src/entities/component_permissions.entity.ts
Normal file
29
server/src/entities/component_permissions.entity.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, OneToMany } from 'typeorm';
|
||||
import { Component } from './component.entity';
|
||||
import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants';
|
||||
import { ComponentUser } from './component_users.entity';
|
||||
|
||||
@Entity('component_permissions')
|
||||
export class ComponentPermission {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'component_id', type: 'uuid', nullable: false })
|
||||
componentId: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: PAGE_PERMISSION_TYPE,
|
||||
})
|
||||
type: PAGE_PERMISSION_TYPE;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@ManyToOne(() => Component, (component) => component.permissions, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'component_id' })
|
||||
component: Component;
|
||||
|
||||
@OneToMany(() => ComponentUser, (componentUser) => componentUser.componentPermission)
|
||||
users: ComponentUser[];
|
||||
}
|
||||
34
server/src/entities/component_users.entity.ts
Normal file
34
server/src/entities/component_users.entity.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { ComponentPermission } from './component_permissions.entity';
|
||||
import { GroupPermissions } from './group_permissions.entity';
|
||||
|
||||
@Entity('component_users')
|
||||
export class ComponentUser {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'component_permissions_id', type: 'uuid' })
|
||||
componentPermissionsId: 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(() => ComponentPermission, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'component_permissions_id' })
|
||||
componentPermission: ComponentPermission;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ 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';
|
||||
import { ComponentUser } from './component_users.entity';
|
||||
|
||||
@Entity({ name: 'permission_groups' })
|
||||
export class GroupPermissions extends BaseEntity {
|
||||
|
|
@ -76,5 +77,8 @@ export class GroupPermissions extends BaseEntity {
|
|||
@OneToMany(() => QueryUser, (queryUser) => queryUser.permissionGroup)
|
||||
queryUsers: QueryUser[];
|
||||
|
||||
@OneToMany(() => ComponentUser, (componentUser) => componentUser.permissionGroup)
|
||||
componentUsers: ComponentUser[];
|
||||
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ 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';
|
||||
import { ComponentUser } from './component_users.entity';
|
||||
|
||||
@Entity({ name: 'users' })
|
||||
export class User extends BaseEntity {
|
||||
|
|
@ -192,6 +193,9 @@ export class User extends BaseEntity {
|
|||
@OneToMany(() => QueryUser, (queryUser) => queryUser.user)
|
||||
queryUsers: QueryUser[];
|
||||
|
||||
@OneToMany(() => ComponentUser, (componentUser) => componentUser.user)
|
||||
componentUsers: ComponentUser[];
|
||||
|
||||
organizationId: string;
|
||||
invitedOrganizationId: string;
|
||||
organizationIds?: Array<string>;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
FEATURE_KEY.CREATE_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.UPDATE_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.DELETE_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS,
|
||||
FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS,
|
||||
FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS,
|
||||
FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS,
|
||||
],
|
||||
App
|
||||
);
|
||||
|
|
@ -64,6 +68,10 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
FEATURE_KEY.CREATE_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.UPDATE_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.DELETE_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS,
|
||||
FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS,
|
||||
FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS,
|
||||
FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS,
|
||||
],
|
||||
App
|
||||
);
|
||||
|
|
@ -80,6 +88,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
FEATURE_KEY.FETCH_USER_GROUPS,
|
||||
FEATURE_KEY.FETCH_PAGE_PERMISSIONS,
|
||||
FEATURE_KEY.FETCH_QUERY_PERMISSIONS,
|
||||
FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS,
|
||||
],
|
||||
App
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,5 +14,9 @@ export const FEATURES: FeaturesConfig = {
|
|||
[FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS]: {},
|
||||
[FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS]: {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,4 +21,8 @@ export enum FEATURE_KEY {
|
|||
CREATE_QUERY_PERMISSIONS = 'create_query_permissions',
|
||||
UPDATE_QUERY_PERMISSIONS = 'update_query_permissions',
|
||||
DELETE_QUERY_PERMISSIONS = 'delete_query_permissions',
|
||||
FETCH_COMPONENT_PERMISSIONS = 'fetch_component_permissions',
|
||||
CREATE_COMPONENT_PERMISSIONS = 'create_component_permissions',
|
||||
UPDATE_COMPONENT_PERMISSIONS = 'update_component_permissions',
|
||||
DELETE_COMPONENT_PERMISSIONS = 'delete_component_permissions',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,4 +127,50 @@ export class AppPermissionsController implements IAppPermissionsController {
|
|||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS)
|
||||
@Get(':appId/components/:componentId')
|
||||
async fetchComponentPermissions(
|
||||
@User() user,
|
||||
@Param('appId') appId: string,
|
||||
@Param('componentId') componentId: string,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS)
|
||||
@Post(':appId/components/:componentId')
|
||||
async createComponentPermissions(
|
||||
@User() user,
|
||||
@Param('appId') appId: string,
|
||||
@Param('componentId') componentId: string,
|
||||
@Body() body: CreatePermissionDto,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS)
|
||||
@Put(':appId/components/:componentId')
|
||||
async updateComponentPermissions(
|
||||
@User() user,
|
||||
@Param('appId') appId: string,
|
||||
@Param('componentId') componentId: string,
|
||||
@Body() body: CreatePermissionDto,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS)
|
||||
@Delete(':appId/components/:componentId')
|
||||
async deleteComponentPermissions(
|
||||
@User() user,
|
||||
@Param('appId') appId: string,
|
||||
@Param('componentId') componentId: string,
|
||||
@Res({ passthrough: true }) response: Response
|
||||
): Promise<any> {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,4 +46,24 @@ export interface IAppPermissionsController {
|
|||
): Promise<any>;
|
||||
|
||||
deleteQueryPermissions(user: User, appId: string, queryId: string, response: Response): Promise<any>;
|
||||
|
||||
fetchComponentPermissions(user: User, appId: string, componentId: string, response: Response): Promise<any>;
|
||||
|
||||
createComponentPermissions(
|
||||
user: User,
|
||||
appId: string,
|
||||
componentId: string,
|
||||
body: CreatePermissionDto,
|
||||
response: Response
|
||||
): Promise<any>;
|
||||
|
||||
updateComponentPermissions(
|
||||
user: User,
|
||||
appId: string,
|
||||
componentId: string,
|
||||
body: CreatePermissionDto,
|
||||
response: Response
|
||||
): Promise<any>;
|
||||
|
||||
deleteComponentPermissions(user: User, appId: string, componentId: string, response: Response): Promise<any>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,8 @@ export interface IUtilService {
|
|||
createQueryPermission(queryId: string, body: CreatePermissionDto): Promise<any>;
|
||||
|
||||
updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise<any>;
|
||||
|
||||
createComponentPermission(componentId: string, body: CreatePermissionDto): Promise<any>;
|
||||
|
||||
updateComponentPermission(componentId: string, body: CreatePermissionDto): Promise<any>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,14 @@ 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 { ComponentUsersRepository } from './repositories/component-users.repository';
|
||||
import { ComponentPermissionsRepository } from './repositories/component-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';
|
||||
import { ComponentUser } from '@entities/component_users.entity';
|
||||
import { ComponentPermission } from '@entities/component_permissions.entity';
|
||||
|
||||
export class AppPermissionsModule {
|
||||
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||
|
|
@ -24,7 +28,16 @@ export class AppPermissionsModule {
|
|||
return {
|
||||
module: AppPermissionsModule,
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([GroupPermissions, User, PageUser, PagePermission, QueryUser, QueryPermission]),
|
||||
TypeOrmModule.forFeature([
|
||||
GroupPermissions,
|
||||
User,
|
||||
PageUser,
|
||||
PagePermission,
|
||||
QueryUser,
|
||||
QueryPermission,
|
||||
ComponentUser,
|
||||
ComponentPermission,
|
||||
]),
|
||||
],
|
||||
controllers: [AppPermissionsController],
|
||||
providers: [
|
||||
|
|
@ -35,6 +48,8 @@ export class AppPermissionsModule {
|
|||
PagePermissionsRepository,
|
||||
QueryUsersRepository,
|
||||
QueryPermissionsRepository,
|
||||
ComponentUsersRepository,
|
||||
ComponentPermissionsRepository,
|
||||
FeatureAbilityFactory,
|
||||
],
|
||||
exports: [AppPermissionsUtilService, AppPermissionsService],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import { ComponentPermission } from '@entities/component_permissions.entity';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, EntityManager, Repository } from 'typeorm';
|
||||
import { ComponentUsersRepository } from './component-users.repository';
|
||||
import { dbTransactionWrap } from '@helpers/database.helper';
|
||||
import { PAGE_PERMISSION_TYPE } from '../constants';
|
||||
|
||||
@Injectable()
|
||||
export class ComponentPermissionsRepository extends Repository<ComponentPermission> {
|
||||
constructor(private dataSource: DataSource, private readonly componentUsersRepository: ComponentUsersRepository) {
|
||||
super(ComponentPermission, dataSource.createEntityManager());
|
||||
}
|
||||
|
||||
async getComponentPermissions(componentId: string, manager?: EntityManager): Promise<ComponentPermission[]> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const componentPermissions = await manager.find(ComponentPermission, {
|
||||
where: { componentId },
|
||||
relations: ['users', 'users.user', 'users.permissionGroup'],
|
||||
});
|
||||
|
||||
return componentPermissions.map((permission) => {
|
||||
if (permission.type === PAGE_PERMISSION_TYPE.GROUP) {
|
||||
return {
|
||||
...permission,
|
||||
groups: permission.users,
|
||||
users: undefined,
|
||||
};
|
||||
}
|
||||
return permission;
|
||||
});
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async createComponentPermissions(
|
||||
componentId: string,
|
||||
type: PAGE_PERMISSION_TYPE,
|
||||
manager?: EntityManager
|
||||
): Promise<ComponentPermission> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const existingPermission = await manager.findOne(ComponentPermission, { where: { componentId } });
|
||||
if (existingPermission) {
|
||||
throw new Error(`Component permission already exists for Component id: ${componentId}`);
|
||||
}
|
||||
|
||||
const componentPermission = manager.create(ComponentPermission, {
|
||||
componentId,
|
||||
type,
|
||||
});
|
||||
return manager.save(componentPermission);
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async deleteComponentPermissions(componentId: string, manager?: EntityManager): Promise<void> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
await manager.delete(ComponentPermission, { componentId });
|
||||
}, manager || this.manager);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { ComponentUser } from '@entities/component_users.entity';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, EntityManager, Repository } from 'typeorm';
|
||||
import { dbTransactionWrap } from '@helpers/database.helper';
|
||||
import { ComponentPermission } from '@entities/component_permissions.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ComponentUsersRepository extends Repository<ComponentUser> {
|
||||
constructor(private dataSource: DataSource) {
|
||||
super(ComponentUser, dataSource.createEntityManager());
|
||||
}
|
||||
|
||||
async createComponentUsersWithSingle(
|
||||
componentPermissionsId: string,
|
||||
users: string[],
|
||||
manager?: EntityManager
|
||||
): Promise<ComponentUser[]> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const componentUsers = users.map((userId) => {
|
||||
return manager.create(ComponentUser, {
|
||||
componentPermissionsId,
|
||||
userId,
|
||||
permissionGroupsId: null,
|
||||
});
|
||||
});
|
||||
return manager.save(componentUsers);
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async createComponentUsersWithGroup(
|
||||
componentPermissionsId: string,
|
||||
groups: string[],
|
||||
manager?: EntityManager
|
||||
): Promise<ComponentUser[]> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const componentUsers = groups.map((permissionGroupsId) => {
|
||||
return manager.create(ComponentUser, {
|
||||
componentPermissionsId,
|
||||
permissionGroupsId,
|
||||
userId: null,
|
||||
});
|
||||
});
|
||||
return manager.save(componentUsers);
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async checkComponentUserWithGroup(
|
||||
componentPermission: ComponentPermission,
|
||||
userId: string,
|
||||
manager?: EntityManager
|
||||
): Promise<boolean> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const result = await manager
|
||||
.createQueryBuilder(ComponentUser, 'component_users')
|
||||
.innerJoin('component_users.permissionGroup', 'group')
|
||||
.innerJoin('group.groupUsers', 'groupUser')
|
||||
.where('component_users.componentPermission = :permissionId', {
|
||||
permissionId: componentPermission.id,
|
||||
})
|
||||
.andWhere('groupUser.userId = :userId', { userId })
|
||||
.getOne();
|
||||
|
||||
return !!result;
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async checkComponentUserWithSingle(
|
||||
componentPermission: ComponentPermission,
|
||||
userId: string,
|
||||
manager?: EntityManager
|
||||
): Promise<boolean> {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const componentUser = await manager.findOne(ComponentUser, {
|
||||
where: {
|
||||
componentPermission: { id: componentPermission.id },
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
return !!componentUser;
|
||||
}, manager || this.manager);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,10 @@ interface Features {
|
|||
[FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS]: FeatureConfig;
|
||||
[FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS]: FeatureConfig;
|
||||
}
|
||||
|
||||
export interface FeaturesConfig {
|
||||
|
|
|
|||
|
|
@ -31,4 +31,12 @@ export class AppPermissionsUtilService implements IUtilService {
|
|||
async updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async createComponentPermission(componentId: string, body: CreatePermissionDto): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async updateComponentPermission(componentId: string, body: CreatePermissionDto): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { EventHandler } from 'src/entities/event_handler.entity';
|
|||
import { CreatePageDto, UpdatePageDto } from '@modules/apps/dto/page';
|
||||
|
||||
export interface IPageService {
|
||||
findPagesForVersion(appVersionId: string): Promise<Page[]>;
|
||||
findPagesForVersion(appVersionId: string, mode?: string): Promise<Page[]>;
|
||||
findOne(id: string): Promise<Page>;
|
||||
createPage(page: CreatePageDto, appVersionId: string): Promise<Page>;
|
||||
clonePage(pageId: string, appVersionId: string): Promise<{ pages: Page[]; events: EventHandler[] }>;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ 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';
|
||||
import { ComponentPermission } from '@entities/component_permissions.entity';
|
||||
import { ComponentUser } from '@entities/component_users.entity';
|
||||
interface AppResourceMappings {
|
||||
defaultDataSourceIdMapping: Record<string, string>;
|
||||
dataQueryMapping: Record<string, string>;
|
||||
|
|
@ -238,6 +240,9 @@ export class AppImportExportService {
|
|||
? await manager
|
||||
.createQueryBuilder(Component, 'components')
|
||||
.leftJoinAndSelect('components.layouts', 'layouts')
|
||||
.leftJoinAndSelect('components.permissions', 'permission')
|
||||
.leftJoinAndSelect('permission.users', 'componentUser')
|
||||
.leftJoinAndSelect('componentUser.permissionGroup', 'permissionGroup')
|
||||
.where('components.pageId IN(:...pageId)', {
|
||||
pageId: pages.map((v) => v.id),
|
||||
})
|
||||
|
|
@ -245,6 +250,21 @@ export class AppImportExportService {
|
|||
.getMany()
|
||||
: [];
|
||||
|
||||
const componentsWithPermissionGroups = components.map((component) => {
|
||||
const groupPermission = component.permissions.find((perm) => perm.type === 'GROUP');
|
||||
|
||||
return {
|
||||
...component,
|
||||
permissions: groupPermission
|
||||
? {
|
||||
permissionGroup: groupPermission.users
|
||||
.map((user) => user.permissionGroup?.name)
|
||||
.filter((name): name is string => Boolean(name)),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const events = await manager
|
||||
.createQueryBuilder(EventHandler, 'event_handlers')
|
||||
.where('event_handlers.appVersionId IN(:...versionId)', {
|
||||
|
|
@ -253,7 +273,7 @@ export class AppImportExportService {
|
|||
.orderBy('event_handlers.created_at', 'ASC')
|
||||
.getMany();
|
||||
|
||||
appToExport['components'] = components;
|
||||
appToExport['components'] = componentsWithPermissionGroups;
|
||||
appToExport['pages'] = pagesWithPermissionGroups;
|
||||
appToExport['events'] = events;
|
||||
appToExport['dataQueries'] = queriesWithPermissionGroups;
|
||||
|
|
@ -953,6 +973,13 @@ export class AppImportExportService {
|
|||
await manager.save(newLayout);
|
||||
}));
|
||||
|
||||
if (component.permissions) {
|
||||
savedComponent.permissions = component.permissions;
|
||||
}
|
||||
|
||||
//create component permissions of component if flag enabled in dto
|
||||
await this.createComponentPermissionsForGroups(savedComponent, user.organizationId, manager);
|
||||
|
||||
const componentEvents = importingEvents.filter((event) => event.sourceId === component.id);
|
||||
|
||||
if (componentEvents.length > 0) {
|
||||
|
|
@ -1383,7 +1410,7 @@ export class AppImportExportService {
|
|||
return pageSettings;
|
||||
}
|
||||
|
||||
async checkIfGroupPermissionsExist(pages, queries, organizationId) {
|
||||
async checkIfGroupPermissionsExist(pages, queries, components, organizationId) {
|
||||
const allGroupNames = new Set<string>();
|
||||
|
||||
for (const page of pages) {
|
||||
|
|
@ -1402,6 +1429,15 @@ export class AppImportExportService {
|
|||
}
|
||||
}
|
||||
|
||||
for (const component of components) {
|
||||
const groupNames = component.permissions?.permissionGroup || [];
|
||||
for (const name of groupNames) {
|
||||
if (!allGroupNames.has(name)) {
|
||||
allGroupNames.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allGroupNames.size) return;
|
||||
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
|
|
@ -1497,6 +1533,41 @@ export class AppImportExportService {
|
|||
await manager.save(queryUsers);
|
||||
}
|
||||
|
||||
async createComponentPermissionsForGroups(component, organizationId: string, manager: EntityManager) {
|
||||
const groupNames = component.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(ComponentPermission, {
|
||||
componentId: component.id,
|
||||
type: PAGE_PERMISSION_TYPE.GROUP,
|
||||
});
|
||||
|
||||
const savedPermission = await manager.save(permission);
|
||||
|
||||
const componentUsers = validGroupNames.map((name) =>
|
||||
manager.create(ComponentUser, {
|
||||
componentPermissionsId: savedPermission.id,
|
||||
permissionGroupsId: groupMap.get(name).id,
|
||||
})
|
||||
);
|
||||
|
||||
await manager.save(componentUsers);
|
||||
}
|
||||
|
||||
async createAppVersionsForImportedApp(
|
||||
manager: EntityManager,
|
||||
user: User,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class PageService implements IPageService {
|
|||
protected eventHandlerService: EventsService
|
||||
) {}
|
||||
|
||||
async findPagesForVersion(appVersionId: string): Promise<Page[]> {
|
||||
async findPagesForVersion(appVersionId: string, mode?: string): Promise<Page[]> {
|
||||
// const allPages = await this.pageRepository.find({ where: { appVersionId }, order: { index: 'ASC' } });
|
||||
const allPages = await this.pageHelperService.fetchPages(appVersionId);
|
||||
const pagesWithComponents = await Promise.all(
|
||||
|
|
|
|||
|
|
@ -79,8 +79,14 @@ export class ImportExportResourcesService {
|
|||
appParams = { ...appParams.appV2 };
|
||||
const pages = appParams?.pages;
|
||||
const queries = appParams?.dataQueries;
|
||||
(pages?.length || queries?.length) &&
|
||||
(await this.appImportExportService.checkIfGroupPermissionsExist(pages, queries, user.organizationId));
|
||||
const components = appParams?.components;
|
||||
(pages?.length || queries?.length || components?.length) &&
|
||||
(await this.appImportExportService.checkIfGroupPermissionsExist(
|
||||
pages,
|
||||
queries,
|
||||
components,
|
||||
user.organizationId
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Get, Put, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { VersionService } from './service';
|
||||
import { InitModule } from '@modules/app/decorators/init-module';
|
||||
import { MODULES } from '@modules/app/constants/modules';
|
||||
|
|
@ -26,8 +26,8 @@ export class VersionControllerV2 implements IVersionControllerV2 {
|
|||
@InitFeature(FEATURE_KEY.GET_ONE)
|
||||
@UseGuards(JwtAuthGuard, ValidAppGuard, FeatureAbilityGuard)
|
||||
@Get(':id/versions/:versionId')
|
||||
getVersion(@User() user: UserEntity, @App() app: AppEntity) {
|
||||
return this.versionService.getVersion(app, user);
|
||||
getVersion(@User() user: UserEntity, @App() app: AppEntity, @Query('mode') mode?: string) {
|
||||
return this.versionService.getVersion(app, user, mode);
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.UPDATE)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { App as AppEntity } from '@entities/app.entity';
|
|||
import { PromoteVersionDto } from '../dto';
|
||||
|
||||
export interface IVersionControllerV2 {
|
||||
getVersion(user: UserEntity, app: AppEntity): Promise<any>;
|
||||
getVersion(user: UserEntity, app: AppEntity, mode?: string): Promise<any>;
|
||||
updateVersion(user: UserEntity, app: AppEntity, appVersionUpdateDto: AppVersionUpdateDto): Promise<any>;
|
||||
updateGlobalSettings(user: UserEntity, app: AppEntity, appVersionUpdateDto: AppVersionUpdateDto): Promise<any>;
|
||||
promoteVersion(user: UserEntity, app: AppEntity, promoteVersionDto: PromoteVersionDto): Promise<any>;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export interface IVersionService {
|
|||
|
||||
deleteVersion(app: App, user: User): Promise<void>;
|
||||
|
||||
getVersion(app: App, user: User): Promise<any>;
|
||||
getVersion(app: App, user: User, mode?: string): Promise<any>;
|
||||
|
||||
update(app: App, user: User, appVersionUpdateDto: AppVersionUpdateDto): Promise<void>;
|
||||
|
||||
|
|
|
|||
|
|
@ -147,6 +147,34 @@ export class VersionRepository extends Repository<AppVersion> {
|
|||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async findVersionWithQueryPermissions(id: string, manager?: EntityManager): Promise<AppVersion> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const appVersion = await manager
|
||||
.createQueryBuilder(AppVersion, 'appVersion')
|
||||
.where('appVersion.id = :id', { id })
|
||||
.leftJoinAndSelect('appVersion.app', 'app')
|
||||
.leftJoinAndSelect('appVersion.dataQueries', 'dataQueries')
|
||||
.leftJoinAndSelect('dataQueries.dataSource', 'dataSource')
|
||||
.leftJoinAndSelect('dataQueries.plugins', 'plugins')
|
||||
.leftJoinAndSelect('plugins.manifestFile', 'manifestFile')
|
||||
.leftJoinAndSelect('dataQueries.permissions', 'permission')
|
||||
.leftJoinAndSelect('permission.users', 'queryUser')
|
||||
.leftJoinAndSelect('queryUser.user', 'user')
|
||||
.leftJoinAndSelect('queryUser.permissionGroup', 'group')
|
||||
.getOneOrFail();
|
||||
|
||||
if (appVersion?.dataQueries) {
|
||||
for (const query of appVersion?.dataQueries) {
|
||||
if (query?.plugin) {
|
||||
query.plugin.manifestFile.data = JSON.parse(decode(query.plugin.manifestFile.data.toString('utf8')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return appVersion;
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
getVersionsInApp(appId: string, manager?: EntityManager): Promise<AppVersion[]> {
|
||||
return dbTransactionWrap((manager: EntityManager) => {
|
||||
return manager.find(AppVersion, {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export class VersionService implements IVersionService {
|
|||
return await this.versionsUtilService.deleteVersion(app, user, manager);
|
||||
}
|
||||
|
||||
async getVersion(app: App, user: User): Promise<any> {
|
||||
async getVersion(app: App, user: User, mode?: string): Promise<any> {
|
||||
const prepareResponse = async (app: App, versionId: string) => {
|
||||
let appVersion,
|
||||
updatedVersionId = versionId;
|
||||
|
|
|
|||
Loading…
Reference in a new issue