From e93adde4b8c11eac93caa6ced59eeb51cb9962c0 Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:01:33 +0530 Subject: [PATCH] Decouple Send Email node from workflows (#13322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Renamed `WorkflowActionAdapter` to `ToolExecutorWorkflowAction` - Renamed `settingPermission` table to `permissionFlag` and `setting` column to `flag` - Decoupled the send email logic from workflows to tools - Add new `Tools Permission` section in FE --------- Co-authored-by: Félix Malfait Co-authored-by: Félix Malfait --- .../src/generated-metadata/graphql.ts | 125 +++++++------- .../twenty-front/src/generated/graphql.ts | 54 +++--- .../modules/app/components/SettingsRoutes.tsx | 16 +- ...rmationBannerBillingSubscriptionPaused.tsx | 4 +- .../InformationBannerEndTrialPeriod.tsx | 4 +- .../InformationBannerFailPaymentInfo.tsx | 4 +- ...InformationBannerNoBillingSubscription.tsx | 4 +- .../hooks/useRecordGroupActions.ts | 4 +- .../SettingsProtectedRouteWrapper.tsx | 4 +- .../useSettingsNavigationItems.test.tsx | 26 +-- .../hooks/useSettingsNavigationItems.tsx | 23 ++- .../fragments/permissionFlagFragment.ts | 9 + .../roles/graphql/fragments/roleFragment.ts | 1 + .../fragments/settingPermissionFragment.ts | 9 - .../upsertPermissionFlagsMutation.ts | 15 ++ .../upsertSettingPermissionsMutation.ts | 15 -- .../roles/graphql/queries/getRolesQuery.ts | 8 +- .../roles/hooks/useHasSettingsPermission.ts | 10 +- .../roles/hooks/useSettingsPermissionMap.ts | 6 +- .../components/SettingsRolePermissions.tsx | 7 +- ...SettingsRolePermissionsSettingsSection.tsx | 22 +-- ...ingsRolePermissionsSettingsTableHeader.tsx | 14 +- ...ettingsRolePermissionsSettingsTableRow.tsx | 33 ++-- .../SettingsRolePermissionsToolSection.tsx | 101 ++++++++++++ ...ettingsRolePermissionsSettingPermission.ts | 5 +- .../roles/role/components/SettingsRole.tsx | 31 ++-- .../components/SettingsRoleCreateEffect.tsx | 1 + .../states/settingsDraftRoleFamilyState.ts | 3 +- .../src/testing/mock-data/roles.ts | 2 + .../src/testing/mock-data/users.ts | 4 +- ...renameSettingPermissionToPermissionFlag.ts | 46 ++++++ ...977613-AddCanAccessAllToolsColumnToRole.ts | 23 +++ ...-with-settings-permissions-requirements.ts | 6 +- .../interfaces/base-resolver-service.ts | 4 +- .../src/engine/core-modules/ai/ai.module.ts | 12 ++ .../ai/services/tool-adapter.service.ts | 53 ++++-- .../engine/core-modules/auth/auth.resolver.ts | 4 +- .../core-modules/billing/billing.resolver.ts | 14 +- .../imap-smtp-caldav-connection.resolver.ts | 4 +- .../engine/core-modules/lab/lab.resolver.ts | 4 +- .../engine/core-modules/sso/sso.resolver.ts | 4 +- .../tool/constants/tools.const.ts | 7 - .../core-modules/tool/enums/tool-type.enum.ts | 1 + .../tool/services/tool-registry.service.ts | 43 +++++ .../exceptions/send-email-tool.exception.ts | 14 ++ .../send-email-tool/send-email-tool.schema.ts | 23 +++ .../tools/send-email-tool/send-email-tool.ts | 155 ++++++++++++++++++ .../types/send-email-input.type.ts | 5 + .../core-modules/tool/types/tool.type.ts | 2 + .../user-workspace/user-workspace.entity.ts | 14 +- .../resolvers/workflow-builder.resolver.ts | 4 +- .../resolvers/workflow-step.resolver.ts | 4 +- .../resolvers/workflow-trigger.resolver.ts | 4 +- .../resolvers/workflow-version.resolver.ts | 4 +- .../workspace-invitation.resolver.ts | 4 +- .../workspace/services/workspace.service.ts | 6 +- .../workspace/workspace.resolver.ts | 6 +- .../guards/settings-permissions.guard.ts | 4 +- .../agent/agent-tool.service.ts | 9 +- .../constants/agent-system-prompts.const.ts | 69 ++------ .../field-metadata/field-metadata.resolver.ts | 8 +- .../object-metadata/object-metadata.module.ts | 6 +- .../object-metadata.resolver.ts | 8 +- .../dtos/permission-flag.dto.ts | 15 ++ .../dtos/upsert-permission-flag-input.ts | 18 ++ .../permission-flag.entity.ts} | 12 +- .../permission-flag/permission-flag.module.ts | 18 ++ .../permission-flag.service.ts} | 54 +++--- ...s.ts => permission-flag-type.constants.ts} | 6 +- .../permissions/permissions.service.ts | 69 ++++++-- .../types/user-workspace-permissions.ts | 4 +- .../role/dtos/create-role-input.dto.ts | 5 + .../metadata-modules/role/dtos/role.dto.ts | 9 +- .../role/dtos/update-role-input.dto.ts | 5 + .../metadata-modules/role/role.entity.ts | 11 +- .../metadata-modules/role/role.module.ts | 4 +- .../metadata-modules/role/role.resolver.ts | 26 +-- .../metadata-modules/role/role.service.ts | 9 +- .../utils/fromRoleEntityToRoleDto.util.ts | 1 + ...ermissionsToUserWorkspacePermissionsDto.ts | 4 +- .../dtos/setting-permission.dto.ts | 15 -- .../dtos/upsert-setting-permission-input.ts | 18 -- .../setting-permission.module.ts | 18 -- .../user-role/user-role.service.ts | 2 +- .../workspace-permissions-cache.service.ts | 10 +- .../engine/twenty-orm/twenty-orm.module.ts | 2 + .../messaging-import-manager.module.ts | 2 - .../src/modules/messaging/messaging.module.ts | 2 +- .../monitoring/messaging-monitoring.module.ts | 2 - .../factories/workflow-action.factory.ts | 10 +- .../mail-sender/send-email-action.module.ts | 12 -- .../mail-sender/send-email.workflow-action.ts | 140 ---------------- ...er.ts => tool-executor-workflow-action.ts} | 9 +- .../workflow-executor.module.ts | 8 +- ...workspace-member-pre-query-hook.service.ts | 4 +- ...r-settings-permissions.integration-spec.ts | 72 ++++---- .../roles.integration-spec.ts | 14 +- .../agent/utils/agent-tool-test-utils.ts | 33 ++++ 98 files changed, 1076 insertions(+), 705 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/roles/graphql/fragments/permissionFlagFragment.ts delete mode 100644 packages/twenty-front/src/modules/settings/roles/graphql/fragments/settingPermissionFragment.ts create mode 100644 packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertPermissionFlagsMutation.ts delete mode 100644 packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertSettingPermissionsMutation.ts rename packages/twenty-front/src/modules/settings/roles/role-permissions/{settings-permissions => permission-flags}/components/SettingsRolePermissionsSettingsSection.tsx (84%) rename packages/twenty-front/src/modules/settings/roles/role-permissions/{settings-permissions => permission-flags}/components/SettingsRolePermissionsSettingsTableHeader.tsx (83%) rename packages/twenty-front/src/modules/settings/roles/role-permissions/{settings-permissions => permission-flags}/components/SettingsRolePermissionsSettingsTableRow.tsx (76%) create mode 100644 packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsToolSection.tsx rename packages/twenty-front/src/modules/settings/roles/role-permissions/{settings-permissions => permission-flags}/types/SettingsRolePermissionsSettingPermission.ts (58%) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1753149175945-renameSettingPermissionToPermissionFlag.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1753318977613-AddCanAccessAllToolsColumnToRole.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/tool/constants/tools.const.ts create mode 100644 packages/twenty-server/src/engine/core-modules/tool/services/tool-registry.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts create mode 100644 packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts create mode 100644 packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts create mode 100644 packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input.ts rename packages/twenty-server/src/engine/metadata-modules/{setting-permission/setting-permission.entity.ts => permission-flag/permission-flag.entity.ts} (64%) create mode 100644 packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts rename packages/twenty-server/src/engine/metadata-modules/{setting-permission/setting-permission.service.ts => permission-flag/permission-flag.service.ts} (65%) rename packages/twenty-server/src/engine/metadata-modules/permissions/constants/{setting-permission-type.constants.ts => permission-flag-type.constants.ts} (67%) delete mode 100644 packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.module.ts delete mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email-action.module.ts delete mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts rename packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/{workflow-action-adapter.ts => tool-executor-workflow-action.ts} (84%) diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index f593e716a5..e51eff3034 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -548,6 +548,7 @@ export type CreateRemoteServerInput = { }; export type CreateRoleInput = { + canAccessAllTools?: InputMaybe; canDestroyAllObjectRecords?: InputMaybe; canReadAllObjectRecords?: InputMaybe; canSoftDeleteAllObjectRecords?: InputMaybe; @@ -1202,7 +1203,7 @@ export type Mutation = { uploadWorkspaceLogo: SignedFileDto; upsertFieldPermissions: Array; upsertObjectPermissions: Array; - upsertSettingPermissions: Array; + upsertPermissionFlags: Array; userLookupAdminPanel: UserLookup; validateApprovedAccessDomain: ApprovedAccessDomain; verifyTwoFactorAuthenticationMethodForAuthenticatedUser: VerifyTwoFactorAuthenticationMethodOutput; @@ -1693,8 +1694,8 @@ export type MutationUpsertObjectPermissionsArgs = { }; -export type MutationUpsertSettingPermissionsArgs = { - upsertSettingPermissionsInput: UpsertSettingPermissionsInput; +export type MutationUpsertPermissionFlagsArgs = { + upsertPermissionFlagsInput: UpsertPermissionFlagsInput; }; @@ -1876,6 +1877,25 @@ export type PageInfo = { startCursor?: Maybe; }; +export type PermissionFlag = { + __typename?: 'PermissionFlag'; + flag: PermissionFlagType; + id: Scalars['String']; + roleId: Scalars['String']; +}; + +export enum PermissionFlagType { + ADMIN_PANEL = 'ADMIN_PANEL', + API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS', + DATA_MODEL = 'DATA_MODEL', + ROLES = 'ROLES', + SECURITY = 'SECURITY', + SEND_EMAIL_TOOL = 'SEND_EMAIL_TOOL', + WORKFLOWS = 'WORKFLOWS', + WORKSPACE = 'WORKSPACE', + WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS' +} + export enum PermissionsOnAllObjectRecords { DESTROY_ALL_OBJECT_RECORDS = 'DESTROY_ALL_OBJECT_RECORDS', READ_ALL_OBJECT_RECORDS = 'READ_ALL_OBJECT_RECORDS', @@ -2250,6 +2270,7 @@ export type RevokeApiKeyDto = { export type Role = { __typename?: 'Role'; + canAccessAllTools: Scalars['Boolean']; canDestroyAllObjectRecords: Scalars['Boolean']; canReadAllObjectRecords: Scalars['Boolean']; canSoftDeleteAllObjectRecords: Scalars['Boolean']; @@ -2261,7 +2282,7 @@ export type Role = { isEditable: Scalars['Boolean']; label: Scalars['String']; objectPermissions?: Maybe>; - settingPermissions?: Maybe>; + permissionFlags?: Maybe>; workspaceMembers: Array; }; @@ -2381,24 +2402,6 @@ export type ServerlessFunctionIdInput = { id: Scalars['ID']; }; -export type SettingPermission = { - __typename?: 'SettingPermission'; - id: Scalars['String']; - roleId: Scalars['String']; - setting: SettingPermissionType; -}; - -export enum SettingPermissionType { - ADMIN_PANEL = 'ADMIN_PANEL', - API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS', - DATA_MODEL = 'DATA_MODEL', - ROLES = 'ROLES', - SECURITY = 'SECURITY', - WORKFLOWS = 'WORKFLOWS', - WORKSPACE = 'WORKSPACE', - WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS' -} - export type SetupOidcSsoInput = { clientID: Scalars['String']; clientSecret: Scalars['String']; @@ -2686,6 +2689,7 @@ export type UpdateRoleInput = { }; export type UpdateRolePayload = { + canAccessAllTools?: InputMaybe; canDestroyAllObjectRecords?: InputMaybe; canReadAllObjectRecords?: InputMaybe; canSoftDeleteAllObjectRecords?: InputMaybe; @@ -2752,9 +2756,9 @@ export type UpsertObjectPermissionsInput = { roleId: Scalars['String']; }; -export type UpsertSettingPermissionsInput = { +export type UpsertPermissionFlagsInput = { + permissionFlagKeys: Array; roleId: Scalars['String']; - settingPermissionKeys: Array; }; export type User = { @@ -2832,7 +2836,7 @@ export type UserWorkspace = { objectPermissions?: Maybe>; /** @deprecated Use objectPermissions instead */ objectRecordsPermissions?: Maybe>; - settingsPermissions?: Maybe>; + settingsPermissions?: Maybe>; twoFactorAuthenticationMethodSummary?: Maybe>; updatedAt: Scalars['DateTime']; user: User; @@ -3654,16 +3658,16 @@ export type UpdateLabPublicFeatureFlagMutation = { __typename?: 'Mutation', upda export type ObjectPermissionFragmentFragment = { __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }; -export type RoleFragmentFragment = { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean }; +export type PermissionFlagFragmentFragment = { __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }; -export type SettingPermissionFragmentFragment = { __typename?: 'SettingPermission', id: string, setting: SettingPermissionType, roleId: string }; +export type RoleFragmentFragment = { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean }; export type CreateOneRoleMutationVariables = Exact<{ createRoleInput: CreateRoleInput; }>; -export type CreateOneRoleMutation = { __typename?: 'Mutation', createOneRole: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } }; +export type CreateOneRoleMutation = { __typename?: 'Mutation', createOneRole: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } }; export type DeleteOneRoleMutationVariables = Exact<{ roleId: Scalars['String']; @@ -3677,7 +3681,7 @@ export type UpdateOneRoleMutationVariables = Exact<{ }>; -export type UpdateOneRoleMutation = { __typename?: 'Mutation', updateOneRole: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } }; +export type UpdateOneRoleMutation = { __typename?: 'Mutation', updateOneRole: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } }; export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{ workspaceMemberId: Scalars['String']; @@ -3685,7 +3689,7 @@ export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{ }>; -export type UpdateWorkspaceMemberRoleMutation = { __typename?: 'Mutation', updateWorkspaceMemberRole: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, roles?: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean }> | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } }; +export type UpdateWorkspaceMemberRoleMutation = { __typename?: 'Mutation', updateWorkspaceMemberRole: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, roles?: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean }> | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } }; export type UpsertObjectPermissionsMutationVariables = Exact<{ upsertObjectPermissionsInput: UpsertObjectPermissionsInput; @@ -3694,17 +3698,17 @@ export type UpsertObjectPermissionsMutationVariables = Exact<{ export type UpsertObjectPermissionsMutation = { __typename?: 'Mutation', upsertObjectPermissions: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> }; -export type UpsertSettingPermissionsMutationVariables = Exact<{ - upsertSettingPermissionsInput: UpsertSettingPermissionsInput; +export type UpsertPermissionFlagsMutationVariables = Exact<{ + upsertPermissionFlagsInput: UpsertPermissionFlagsInput; }>; -export type UpsertSettingPermissionsMutation = { __typename?: 'Mutation', upsertSettingPermissions: Array<{ __typename?: 'SettingPermission', id: string, setting: SettingPermissionType, roleId: string }> }; +export type UpsertPermissionFlagsMutation = { __typename?: 'Mutation', upsertPermissionFlags: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> }; export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>; -export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, settingPermissions?: Array<{ __typename?: 'SettingPermission', id: string, setting: SettingPermissionType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null }> }; +export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null }> }; export type CreateApprovedAccessDomainMutationVariables = Exact<{ input: CreateApprovedAccessDomainInput; @@ -3835,7 +3839,7 @@ export type VerifyTwoFactorAuthenticationMethodForAuthenticatedUserMutationVaria export type VerifyTwoFactorAuthenticationMethodForAuthenticatedUserMutation = { __typename?: 'Mutation', verifyTwoFactorAuthenticationMethodForAuthenticatedUser: { __typename?: 'VerifyTwoFactorAuthenticationMethodOutput', success: boolean } }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } }; export type WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }; @@ -3854,7 +3858,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } }; export type ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -3973,7 +3977,7 @@ export type UpdateWorkspaceMutationVariables = Exact<{ }>; -export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: any, customDomain?: string | null, subdomain: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isTwoFactorAuthenticationEnforced: boolean, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } }; +export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: any, customDomain?: string | null, subdomain: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isTwoFactorAuthenticationEnforced: boolean, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } }; export type UploadWorkspaceLogoMutationVariables = Exact<{ file: Scalars['Upload']; @@ -4063,10 +4067,10 @@ export const WebhookFragmentFragmentDoc = gql` secret } `; -export const SettingPermissionFragmentFragmentDoc = gql` - fragment SettingPermissionFragment on SettingPermission { +export const PermissionFlagFragmentFragmentDoc = gql` + fragment PermissionFlagFragment on PermissionFlag { id - setting + flag roleId } `; @@ -4133,6 +4137,7 @@ export const RoleFragmentFragmentDoc = gql` description icon canUpdateAllSettings + canAccessAllTools isEditable canReadAllObjectRecords canUpdateAllObjectRecords @@ -7732,41 +7737,39 @@ export function useUpsertObjectPermissionsMutation(baseOptions?: Apollo.Mutation export type UpsertObjectPermissionsMutationHookResult = ReturnType; export type UpsertObjectPermissionsMutationResult = Apollo.MutationResult; export type UpsertObjectPermissionsMutationOptions = Apollo.BaseMutationOptions; -export const UpsertSettingPermissionsDocument = gql` - mutation UpsertSettingPermissions($upsertSettingPermissionsInput: UpsertSettingPermissionsInput!) { - upsertSettingPermissions( - upsertSettingPermissionsInput: $upsertSettingPermissionsInput - ) { - ...SettingPermissionFragment +export const UpsertPermissionFlagsDocument = gql` + mutation UpsertPermissionFlags($upsertPermissionFlagsInput: UpsertPermissionFlagsInput!) { + upsertPermissionFlags(upsertPermissionFlagsInput: $upsertPermissionFlagsInput) { + ...PermissionFlagFragment } } - ${SettingPermissionFragmentFragmentDoc}`; -export type UpsertSettingPermissionsMutationFn = Apollo.MutationFunction; + ${PermissionFlagFragmentFragmentDoc}`; +export type UpsertPermissionFlagsMutationFn = Apollo.MutationFunction; /** - * __useUpsertSettingPermissionsMutation__ + * __useUpsertPermissionFlagsMutation__ * - * To run a mutation, you first call `useUpsertSettingPermissionsMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUpsertSettingPermissionsMutation` returns a tuple that includes: + * To run a mutation, you first call `useUpsertPermissionFlagsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpsertPermissionFlagsMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example - * const [upsertSettingPermissionsMutation, { data, loading, error }] = useUpsertSettingPermissionsMutation({ + * const [upsertPermissionFlagsMutation, { data, loading, error }] = useUpsertPermissionFlagsMutation({ * variables: { - * upsertSettingPermissionsInput: // value for 'upsertSettingPermissionsInput' + * upsertPermissionFlagsInput: // value for 'upsertPermissionFlagsInput' * }, * }); */ -export function useUpsertSettingPermissionsMutation(baseOptions?: Apollo.MutationHookOptions) { +export function useUpsertPermissionFlagsMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(UpsertSettingPermissionsDocument, options); + return Apollo.useMutation(UpsertPermissionFlagsDocument, options); } -export type UpsertSettingPermissionsMutationHookResult = ReturnType; -export type UpsertSettingPermissionsMutationResult = Apollo.MutationResult; -export type UpsertSettingPermissionsMutationOptions = Apollo.BaseMutationOptions; +export type UpsertPermissionFlagsMutationHookResult = ReturnType; +export type UpsertPermissionFlagsMutationResult = Apollo.MutationResult; +export type UpsertPermissionFlagsMutationOptions = Apollo.BaseMutationOptions; export const GetRolesDocument = gql` query GetRoles { getRoles { @@ -7774,8 +7777,8 @@ export const GetRolesDocument = gql` workspaceMembers { ...WorkspaceMemberQueryFragment } - settingPermissions { - ...SettingPermissionFragment + permissionFlags { + ...PermissionFlagFragment } objectPermissions { ...ObjectPermissionFragment @@ -7784,7 +7787,7 @@ export const GetRolesDocument = gql` } ${RoleFragmentFragmentDoc} ${WorkspaceMemberQueryFragmentFragmentDoc} -${SettingPermissionFragmentFragmentDoc} +${PermissionFlagFragmentFragmentDoc} ${ObjectPermissionFragmentFragmentDoc}`; /** diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 95477b938a..639c1f725b 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -512,6 +512,7 @@ export type CreateOneFieldMetadataInput = { }; export type CreateRoleInput = { + canAccessAllTools?: InputMaybe; canDestroyAllObjectRecords?: InputMaybe; canReadAllObjectRecords?: InputMaybe; canSoftDeleteAllObjectRecords?: InputMaybe; @@ -1153,7 +1154,7 @@ export type Mutation = { uploadWorkspaceLogo: SignedFileDto; upsertFieldPermissions: Array; upsertObjectPermissions: Array; - upsertSettingPermissions: Array; + upsertPermissionFlags: Array; userLookupAdminPanel: UserLookup; validateApprovedAccessDomain: ApprovedAccessDomain; verifyTwoFactorAuthenticationMethodForAuthenticatedUser: VerifyTwoFactorAuthenticationMethodOutput; @@ -1604,8 +1605,8 @@ export type MutationUpsertObjectPermissionsArgs = { }; -export type MutationUpsertSettingPermissionsArgs = { - upsertSettingPermissionsInput: UpsertSettingPermissionsInput; +export type MutationUpsertPermissionFlagsArgs = { + upsertPermissionFlagsInput: UpsertPermissionFlagsInput; }; @@ -1787,6 +1788,25 @@ export type PageInfo = { startCursor?: Maybe; }; +export type PermissionFlag = { + __typename?: 'PermissionFlag'; + flag: PermissionFlagType; + id: Scalars['String']; + roleId: Scalars['String']; +}; + +export enum PermissionFlagType { + ADMIN_PANEL = 'ADMIN_PANEL', + API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS', + DATA_MODEL = 'DATA_MODEL', + ROLES = 'ROLES', + SECURITY = 'SECURITY', + SEND_EMAIL_TOOL = 'SEND_EMAIL_TOOL', + WORKFLOWS = 'WORKFLOWS', + WORKSPACE = 'WORKSPACE', + WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS' +} + export enum PermissionsOnAllObjectRecords { DESTROY_ALL_OBJECT_RECORDS = 'DESTROY_ALL_OBJECT_RECORDS', READ_ALL_OBJECT_RECORDS = 'READ_ALL_OBJECT_RECORDS', @@ -2096,6 +2116,7 @@ export type RevokeApiKeyDto = { export type Role = { __typename?: 'Role'; + canAccessAllTools: Scalars['Boolean']; canDestroyAllObjectRecords: Scalars['Boolean']; canReadAllObjectRecords: Scalars['Boolean']; canSoftDeleteAllObjectRecords: Scalars['Boolean']; @@ -2107,7 +2128,7 @@ export type Role = { isEditable: Scalars['Boolean']; label: Scalars['String']; objectPermissions?: Maybe>; - settingPermissions?: Maybe>; + permissionFlags?: Maybe>; workspaceMembers: Array; }; @@ -2227,24 +2248,6 @@ export type ServerlessFunctionIdInput = { id: Scalars['ID']; }; -export type SettingPermission = { - __typename?: 'SettingPermission'; - id: Scalars['String']; - roleId: Scalars['String']; - setting: SettingPermissionType; -}; - -export enum SettingPermissionType { - ADMIN_PANEL = 'ADMIN_PANEL', - API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS', - DATA_MODEL = 'DATA_MODEL', - ROLES = 'ROLES', - SECURITY = 'SECURITY', - WORKFLOWS = 'WORKFLOWS', - WORKSPACE = 'WORKSPACE', - WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS' -} - export type SetupOidcSsoInput = { clientID: Scalars['String']; clientSecret: Scalars['String']; @@ -2524,6 +2527,7 @@ export type UpdateRoleInput = { }; export type UpdateRolePayload = { + canAccessAllTools?: InputMaybe; canDestroyAllObjectRecords?: InputMaybe; canReadAllObjectRecords?: InputMaybe; canSoftDeleteAllObjectRecords?: InputMaybe; @@ -2590,9 +2594,9 @@ export type UpsertObjectPermissionsInput = { roleId: Scalars['String']; }; -export type UpsertSettingPermissionsInput = { +export type UpsertPermissionFlagsInput = { + permissionFlagKeys: Array; roleId: Scalars['String']; - settingPermissionKeys: Array; }; export type User = { @@ -2660,7 +2664,7 @@ export type UserWorkspace = { objectPermissions?: Maybe>; /** @deprecated Use objectPermissions instead */ objectRecordsPermissions?: Maybe>; - settingsPermissions?: Maybe>; + settingsPermissions?: Maybe>; twoFactorAuthenticationMethodSummary?: Maybe>; updatedAt: Scalars['DateTime']; user: User; diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 5b5126f472..f8db3e5905 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -4,7 +4,7 @@ import { Route, Routes } from 'react-router-dom'; import { SettingsProtectedRouteWrapper } from '@/settings/components/SettingsProtectedRouteWrapper'; import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; import { SettingsPath } from '@/types/SettingsPath'; -import { SettingPermissionType } from '~/generated/graphql'; +import { PermissionFlagType } from '~/generated/graphql'; const SettingsApiKeys = lazy(() => import('~/pages/settings/developers/api-keys/SettingsApiKeys').then( @@ -405,7 +405,7 @@ export const SettingsRoutes = ({ } > @@ -416,7 +416,7 @@ export const SettingsRoutes = ({ } > @@ -428,7 +428,7 @@ export const SettingsRoutes = ({ } > @@ -458,7 +458,7 @@ export const SettingsRoutes = ({ } > @@ -480,7 +480,7 @@ export const SettingsRoutes = ({ } > @@ -555,7 +555,7 @@ export const SettingsRoutes = ({ } > @@ -587,7 +587,7 @@ export const SettingsRoutes = ({ } > diff --git a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerBillingSubscriptionPaused.tsx b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerBillingSubscriptionPaused.tsx index 0f81ac1909..521f31cc2f 100644 --- a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerBillingSubscriptionPaused.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerBillingSubscriptionPaused.tsx @@ -5,7 +5,7 @@ import { SettingsPath } from '@/types/SettingsPath'; import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; import { - SettingPermissionType, + PermissionFlagType, useBillingPortalSessionQuery, } from '~/generated-metadata/graphql'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; @@ -20,7 +20,7 @@ export const InformationBannerBillingSubscriptionPaused = () => { }); const { - [SettingPermissionType.WORKSPACE]: hasPermissionToUpdateBillingDetails, + [PermissionFlagType.WORKSPACE]: hasPermissionToUpdateBillingDetails, } = useSettingsPermissionMap(); const openBillingPortal = () => { diff --git a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerEndTrialPeriod.tsx b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerEndTrialPeriod.tsx index 70533cedb7..07e8b421e3 100644 --- a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerEndTrialPeriod.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerEndTrialPeriod.tsx @@ -2,13 +2,13 @@ import { useEndSubscriptionTrialPeriod } from '@/billing/hooks/useEndSubscriptio import { InformationBanner } from '@/information-banner/components/InformationBanner'; import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap'; import { useLingui } from '@lingui/react/macro'; -import { SettingPermissionType } from '~/generated-metadata/graphql'; +import { PermissionFlagType } from '~/generated-metadata/graphql'; export const InformationBannerEndTrialPeriod = () => { const { endTrialPeriod, isLoading } = useEndSubscriptionTrialPeriod(); const { t } = useLingui(); - const { [SettingPermissionType.WORKSPACE]: hasPermissionToEndTrialPeriod } = + const { [PermissionFlagType.WORKSPACE]: hasPermissionToEndTrialPeriod } = useSettingsPermissionMap(); return ( diff --git a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerFailPaymentInfo.tsx b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerFailPaymentInfo.tsx index 78f2aeae9d..6776a46ab0 100644 --- a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerFailPaymentInfo.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerFailPaymentInfo.tsx @@ -5,7 +5,7 @@ import { SettingsPath } from '@/types/SettingsPath'; import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; import { - SettingPermissionType, + PermissionFlagType, useBillingPortalSessionQuery, } from '~/generated-metadata/graphql'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; @@ -20,7 +20,7 @@ export const InformationBannerFailPaymentInfo = () => { }); const { - [SettingPermissionType.WORKSPACE]: hasPermissionToUpdateBillingDetails, + [PermissionFlagType.WORKSPACE]: hasPermissionToUpdateBillingDetails, } = useSettingsPermissionMap(); const openBillingPortal = () => { diff --git a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerNoBillingSubscription.tsx b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerNoBillingSubscription.tsx index 3f7b6a19f6..8c889315d0 100644 --- a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerNoBillingSubscription.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerNoBillingSubscription.tsx @@ -4,7 +4,7 @@ import { InformationBanner } from '@/information-banner/components/InformationBa import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap'; import { SettingsPath } from '@/types/SettingsPath'; import { t } from '@lingui/core/macro'; -import { SettingPermissionType } from '~/generated-metadata/graphql'; +import { PermissionFlagType } from '~/generated-metadata/graphql'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; export const InformationBannerNoBillingSubscription = () => { @@ -15,7 +15,7 @@ export const InformationBannerNoBillingSubscription = () => { successUrlPath: getSettingsPath(SettingsPath.Billing), }); - const { [SettingPermissionType.WORKSPACE]: hasPermissionToSubscribe } = + const { [PermissionFlagType.WORKSPACE]: hasPermissionToSubscribe } = useSettingsPermissionMap(); return ( diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts index dd10a2294c..cca79f0a5d 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts @@ -24,7 +24,7 @@ import { IconEyeOff, IconSettings, } from 'twenty-ui/display'; -import { SettingPermissionType } from '~/generated/graphql'; +import { PermissionFlagType } from '~/generated/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; type UseRecordGroupActionsParams = { @@ -91,7 +91,7 @@ export const useRecordGroupActions = ({ ]); const hasAccessToDataModelSettings = useHasSettingsPermission( - SettingPermissionType.DATA_MODEL, + PermissionFlagType.DATA_MODEL, ); const currentIndex = visibleRecordGroupIds.findIndex( (id) => id === recordGroupDefinition.id, diff --git a/packages/twenty-front/src/modules/settings/components/SettingsProtectedRouteWrapper.tsx b/packages/twenty-front/src/modules/settings/components/SettingsProtectedRouteWrapper.tsx index 21f5e26766..6a3fbb933f 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsProtectedRouteWrapper.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsProtectedRouteWrapper.tsx @@ -3,12 +3,12 @@ import { SettingsPath } from '@/types/SettingsPath'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { ReactNode } from 'react'; import { Navigate, Outlet } from 'react-router-dom'; -import { FeatureFlagKey, SettingPermissionType } from '~/generated/graphql'; +import { FeatureFlagKey, PermissionFlagType } from '~/generated/graphql'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; type SettingsProtectedRouteWrapperProps = { children?: ReactNode; - settingsPermission?: SettingPermissionType; + settingsPermission?: PermissionFlagType; requiredFeatureFlag?: FeatureFlagKey; }; diff --git a/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx b/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx index 7bade12cc0..ff8882a044 100644 --- a/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx +++ b/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx @@ -7,7 +7,7 @@ import { MutableSnapshot, RecoilRoot } from 'recoil'; import { Billing, OnboardingStatus, - SettingPermissionType, + PermissionFlagType, } from '~/generated/graphql'; import { currentUserState } from '@/auth/states/currentUserState'; @@ -60,12 +60,12 @@ jest.mock('@/settings/roles/hooks/useSettingsPermissionMap', () => ({ describe('useSettingsNavigationItems', () => { it('should hide workspace settings when no permissions', () => { (useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({ - [SettingPermissionType.WORKSPACE]: false, - [SettingPermissionType.WORKSPACE_MEMBERS]: false, - [SettingPermissionType.DATA_MODEL]: false, - [SettingPermissionType.API_KEYS_AND_WEBHOOKS]: false, - [SettingPermissionType.ROLES]: false, - [SettingPermissionType.SECURITY]: false, + [PermissionFlagType.WORKSPACE]: false, + [PermissionFlagType.WORKSPACE_MEMBERS]: false, + [PermissionFlagType.DATA_MODEL]: false, + [PermissionFlagType.API_KEYS_AND_WEBHOOKS]: false, + [PermissionFlagType.ROLES]: false, + [PermissionFlagType.SECURITY]: false, })); const { result } = renderHook(() => useSettingsNavigationItems(), { @@ -81,12 +81,12 @@ describe('useSettingsNavigationItems', () => { it('should show workspace settings when has permissions', () => { (useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({ - [SettingPermissionType.WORKSPACE]: true, - [SettingPermissionType.WORKSPACE_MEMBERS]: true, - [SettingPermissionType.DATA_MODEL]: true, - [SettingPermissionType.API_KEYS_AND_WEBHOOKS]: true, - [SettingPermissionType.ROLES]: true, - [SettingPermissionType.SECURITY]: true, + [PermissionFlagType.WORKSPACE]: true, + [PermissionFlagType.WORKSPACE_MEMBERS]: true, + [PermissionFlagType.DATA_MODEL]: true, + [PermissionFlagType.API_KEYS_AND_WEBHOOKS]: true, + [PermissionFlagType.ROLES]: true, + [PermissionFlagType.SECURITY]: true, })); const { result } = renderHook(() => useSettingsNavigationItems(), { diff --git a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx index 7b3e374dca..c52656b66c 100644 --- a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx +++ b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx @@ -30,7 +30,7 @@ import { IconUsers, IconWebhook, } from 'twenty-ui/display'; -import { SettingPermissionType } from '~/generated/graphql'; +import { PermissionFlagType } from '~/generated/graphql'; export type SettingsNavigationSection = { label: string; @@ -107,46 +107,45 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { label: t`General`, path: SettingsPath.Workspace, Icon: IconSettings, - isHidden: !permissionMap[SettingPermissionType.WORKSPACE], + isHidden: !permissionMap[PermissionFlagType.WORKSPACE], }, { label: t`Members`, path: SettingsPath.WorkspaceMembersPage, Icon: IconUsers, - isHidden: !permissionMap[SettingPermissionType.WORKSPACE_MEMBERS], + isHidden: !permissionMap[PermissionFlagType.WORKSPACE_MEMBERS], }, { label: t`Roles`, path: SettingsPath.Roles, Icon: IconLock, - isHidden: !permissionMap[SettingPermissionType.ROLES], + isHidden: !permissionMap[PermissionFlagType.ROLES], }, { label: t`Billing`, path: SettingsPath.Billing, Icon: IconCurrencyDollar, isHidden: - !isBillingEnabled || - !permissionMap[SettingPermissionType.WORKSPACE], + !isBillingEnabled || !permissionMap[PermissionFlagType.WORKSPACE], }, { label: t`Data model`, path: SettingsPath.Objects, Icon: IconHierarchy2, - isHidden: !permissionMap[SettingPermissionType.DATA_MODEL], + isHidden: !permissionMap[PermissionFlagType.DATA_MODEL], }, { label: t`Integrations`, path: SettingsPath.Integrations, Icon: IconApps, - isHidden: !permissionMap[SettingPermissionType.API_KEYS_AND_WEBHOOKS], + isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS], }, { label: t`Security`, path: SettingsPath.Security, Icon: IconKey, isAdvanced: true, - isHidden: !permissionMap[SettingPermissionType.SECURITY], + isHidden: !permissionMap[PermissionFlagType.SECURITY], }, ], }, @@ -159,14 +158,14 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { path: SettingsPath.APIs, Icon: IconApi, isAdvanced: true, - isHidden: !permissionMap[SettingPermissionType.API_KEYS_AND_WEBHOOKS], + isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS], }, { label: t`Webhooks`, path: SettingsPath.Webhooks, Icon: IconWebhook, isAdvanced: true, - isHidden: !permissionMap[SettingPermissionType.API_KEYS_AND_WEBHOOKS], + isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS], }, { label: t`Functions`, @@ -192,7 +191,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { Icon: IconFlask, isHidden: !labPublicFeatureFlags.length || - !permissionMap[SettingPermissionType.WORKSPACE], + !permissionMap[PermissionFlagType.WORKSPACE], }, { label: t`Releases`, diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/permissionFlagFragment.ts b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/permissionFlagFragment.ts new file mode 100644 index 0000000000..58c1ef429a --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/permissionFlagFragment.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const PERMISSION_FLAG_FRAGMENT = gql` + fragment PermissionFlagFragment on PermissionFlag { + id + flag + roleId + } +`; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/roleFragment.ts b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/roleFragment.ts index 229e708198..fd733237fa 100644 --- a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/roleFragment.ts +++ b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/roleFragment.ts @@ -7,6 +7,7 @@ export const ROLE_FRAGMENT = gql` description icon canUpdateAllSettings + canAccessAllTools isEditable canReadAllObjectRecords canUpdateAllObjectRecords diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/settingPermissionFragment.ts b/packages/twenty-front/src/modules/settings/roles/graphql/fragments/settingPermissionFragment.ts deleted file mode 100644 index c61f457b26..0000000000 --- a/packages/twenty-front/src/modules/settings/roles/graphql/fragments/settingPermissionFragment.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { gql } from '@apollo/client'; - -export const SETTING_PERMISSION_FRAGMENT = gql` - fragment SettingPermissionFragment on SettingPermission { - id - setting - roleId - } -`; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertPermissionFlagsMutation.ts b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertPermissionFlagsMutation.ts new file mode 100644 index 0000000000..2e4d1c4759 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertPermissionFlagsMutation.ts @@ -0,0 +1,15 @@ +import { PERMISSION_FLAG_FRAGMENT } from '@/settings/roles/graphql/fragments/permissionFlagFragment'; +import { gql } from '@apollo/client'; + +export const UPSERT_PERMISSION_FLAGS = gql` + ${PERMISSION_FLAG_FRAGMENT} + mutation UpsertPermissionFlags( + $upsertPermissionFlagsInput: UpsertPermissionFlagsInput! + ) { + upsertPermissionFlags( + upsertPermissionFlagsInput: $upsertPermissionFlagsInput + ) { + ...PermissionFlagFragment + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertSettingPermissionsMutation.ts b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertSettingPermissionsMutation.ts deleted file mode 100644 index 8923e8f016..0000000000 --- a/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertSettingPermissionsMutation.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SETTING_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/settingPermissionFragment'; -import { gql } from '@apollo/client'; - -export const UPSERT_SETTING_PERMISSIONS = gql` - ${SETTING_PERMISSION_FRAGMENT} - mutation UpsertSettingPermissions( - $upsertSettingPermissionsInput: UpsertSettingPermissionsInput! - ) { - upsertSettingPermissions( - upsertSettingPermissionsInput: $upsertSettingPermissionsInput - ) { - ...SettingPermissionFragment - } - } -`; diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts b/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts index 902afb8dfc..d83cd37a30 100644 --- a/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts +++ b/packages/twenty-front/src/modules/settings/roles/graphql/queries/getRolesQuery.ts @@ -1,13 +1,13 @@ import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment'; +import { PERMISSION_FLAG_FRAGMENT } from '@/settings/roles/graphql/fragments/permissionFlagFragment'; import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment'; -import { SETTING_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/settingPermissionFragment'; import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment'; import { gql } from '@apollo/client'; export const GET_ROLES = gql` ${WORKSPACE_MEMBER_QUERY_FRAGMENT} ${ROLE_FRAGMENT} - ${SETTING_PERMISSION_FRAGMENT} + ${PERMISSION_FLAG_FRAGMENT} ${OBJECT_PERMISSION_FRAGMENT} query GetRoles { getRoles { @@ -15,8 +15,8 @@ export const GET_ROLES = gql` workspaceMembers { ...WorkspaceMemberQueryFragment } - settingPermissions { - ...SettingPermissionFragment + permissionFlags { + ...PermissionFlagFragment } objectPermissions { ...ObjectPermissionFragment diff --git a/packages/twenty-front/src/modules/settings/roles/hooks/useHasSettingsPermission.ts b/packages/twenty-front/src/modules/settings/roles/hooks/useHasSettingsPermission.ts index 8c2d3421ac..75b27fabaf 100644 --- a/packages/twenty-front/src/modules/settings/roles/hooks/useHasSettingsPermission.ts +++ b/packages/twenty-front/src/modules/settings/roles/hooks/useHasSettingsPermission.ts @@ -2,20 +2,20 @@ import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceSta import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useRecoilValue } from 'recoil'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; -import { SettingPermissionType } from '~/generated/graphql'; +import { PermissionFlagType } from '~/generated/graphql'; export const useHasSettingsPermission = ( - settingsPermission?: SettingPermissionType, + permissionFlag?: PermissionFlagType, ) => { const currentWorkspace = useRecoilValue(currentWorkspaceState); const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState); - if (!settingsPermission) { + if (!permissionFlag) { return true; } if ( - settingsPermission === SettingPermissionType.WORKSPACE && + permissionFlag === PermissionFlagType.WORKSPACE && currentWorkspace?.activationStatus === WorkspaceActivationStatus.PENDING_CREATION ) { @@ -28,5 +28,5 @@ export const useHasSettingsPermission = ( return false; } - return currentUserWorkspaceSetting.includes(settingsPermission); + return currentUserWorkspaceSetting.includes(permissionFlag); }; diff --git a/packages/twenty-front/src/modules/settings/roles/hooks/useSettingsPermissionMap.ts b/packages/twenty-front/src/modules/settings/roles/hooks/useSettingsPermissionMap.ts index 50e2bd804a..fbf0df912f 100644 --- a/packages/twenty-front/src/modules/settings/roles/hooks/useSettingsPermissionMap.ts +++ b/packages/twenty-front/src/modules/settings/roles/hooks/useSettingsPermissionMap.ts @@ -1,10 +1,10 @@ import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { useRecoilValue } from 'recoil'; -import { SettingPermissionType } from '~/generated/graphql'; +import { PermissionFlagType } from '~/generated/graphql'; import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue'; export const useSettingsPermissionMap = (): Record< - SettingPermissionType, + PermissionFlagType, boolean > => { const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState); @@ -13,7 +13,7 @@ export const useSettingsPermissionMap = (): Record< currentUserWorkspace?.settingsPermissions; const initialPermissions = buildRecordFromKeysWithSameValue( - Object.values(SettingPermissionType), + Object.values(PermissionFlagType), false, ); diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx index cff402c549..bf5a323e5f 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx @@ -1,6 +1,7 @@ import { SettingsRolePermissionsObjectLevelSection } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection'; import { SettingsRolePermissionsObjectsSection } from '@/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsSection'; -import { SettingsRolePermissionsSettingsSection } from '@/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsSection'; +import { SettingsRolePermissionsSettingsSection } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsSection'; +import { SettingsRolePermissionsToolSection } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsToolSection'; import styled from '@emotion/styled'; const StyledRolePermissionsContainer = styled.div` @@ -32,6 +33,10 @@ export const SettingsRolePermissions = ({ roleId={roleId} isEditable={isEditable} /> + ); }; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsSection.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsSection.tsx similarity index 84% rename from packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsSection.tsx rename to packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsSection.tsx index 7b4cc69701..c2474b7fd9 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsSection.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsSection.tsx @@ -1,7 +1,7 @@ import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle'; -import { SettingsRolePermissionsSettingsTableHeader } from '@/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableHeader'; -import { SettingsRolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableRow'; -import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/settings-permissions/types/SettingsRolePermissionsSettingPermission'; +import { SettingsRolePermissionsSettingsTableHeader } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableHeader'; +import { SettingsRolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableRow'; +import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission'; import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; @@ -17,7 +17,7 @@ import { IconUsers, } from 'twenty-ui/display'; import { AnimatedExpandableContainer, Card, Section } from 'twenty-ui/layout'; -import { SettingPermissionType } from '~/generated-metadata/graphql'; +import { PermissionFlagType } from '~/generated-metadata/graphql'; const StyledTable = styled.div` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; @@ -48,43 +48,43 @@ export const SettingsRolePermissionsSettingsSection = ({ const settingsPermissionsConfig: SettingsRolePermissionsSettingPermission[] = [ { - key: SettingPermissionType.API_KEYS_AND_WEBHOOKS, + key: PermissionFlagType.API_KEYS_AND_WEBHOOKS, name: t`API Keys & Webhooks`, description: t`Manage API keys and webhooks`, Icon: IconCode, }, { - key: SettingPermissionType.WORKSPACE, + key: PermissionFlagType.WORKSPACE, name: t`Workspace`, description: t`Set global workspace preferences`, Icon: IconSettings, }, { - key: SettingPermissionType.WORKSPACE_MEMBERS, + key: PermissionFlagType.WORKSPACE_MEMBERS, name: t`Users`, description: t`Add or remove users`, Icon: IconUsers, }, { - key: SettingPermissionType.ROLES, + key: PermissionFlagType.ROLES, name: t`Roles`, description: t`Define user roles and access levels`, Icon: IconLockOpen, }, { - key: SettingPermissionType.DATA_MODEL, + key: PermissionFlagType.DATA_MODEL, name: t`Data Model`, description: t`Edit CRM data structure and fields`, Icon: IconHierarchy, }, { - key: SettingPermissionType.SECURITY, + key: PermissionFlagType.SECURITY, name: t`Security`, description: t`Manage security policies`, Icon: IconKey, }, { - key: SettingPermissionType.WORKFLOWS, + key: PermissionFlagType.WORKFLOWS, name: t`Workflows`, description: t`Manage workflows`, Icon: IconSettingsAutomation, diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableHeader.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableHeader.tsx similarity index 83% rename from packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableHeader.tsx rename to packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableHeader.tsx index ea457b9f96..522e640845 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableHeader.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableHeader.tsx @@ -4,7 +4,7 @@ import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { Checkbox } from 'twenty-ui/input'; -import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/settings-permissions/types/SettingsRolePermissionsSettingPermission'; +import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission'; import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; import { useRecoilState } from 'recoil'; import { v4 } from 'uuid'; @@ -32,15 +32,15 @@ export const SettingsRolePermissionsSettingsTableHeader = ({ ); const allSettingsPermissionsEnabled = settingsPermissionsConfig.every( (permission) => - settingsDraftRole.settingPermissions?.some( - (settingPermission) => settingPermission.setting === permission.key, + settingsDraftRole.permissionFlags?.some( + (permissionFlag) => permissionFlag.flag === permission.key, ), ); const someSettingsPermissionsEnabled = settingsPermissionsConfig.some( (permission) => - settingsDraftRole.settingPermissions?.some( - (settingPermission) => settingPermission.setting === permission.key, + settingsDraftRole.permissionFlags?.some( + (permissionFlag) => permissionFlag.flag === permission.key, ), ); @@ -61,10 +61,10 @@ export const SettingsRolePermissionsSettingsTableHeader = ({ setSettingsDraftRole({ ...settingsDraftRole, - settingPermissions: newValue + permissionFlags: newValue ? settingsPermissionsConfig.map((permission) => ({ id: v4(), - setting: permission.key, + flag: permission.key, roleId, })) : [], diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableRow.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableRow.tsx similarity index 76% rename from packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableRow.tsx rename to packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableRow.tsx index bfbdffb3aa..20b4391fad 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableRow.tsx @@ -1,4 +1,4 @@ -import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/settings-permissions/types/SettingsRolePermissionsSettingPermission'; +import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission'; import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; @@ -44,6 +44,7 @@ type SettingsRolePermissionsSettingsTableRowProps = { roleId: string; permission: SettingsRolePermissionsSettingPermission; isEditable: boolean; + isToolPermission?: boolean; }; export const SettingsRolePermissionsSettingsTableRow = ({ @@ -55,27 +56,35 @@ export const SettingsRolePermissionsSettingsTableRow = ({ const [settingsDraftRole, setSettingsDraftRole] = useRecoilState( settingsDraftRoleFamilyState(roleId), ); - const canUpdateAllSettings = settingsDraftRole.canUpdateAllSettings; - const isSettingPermissionEnabled = - settingsDraftRole.settingPermissions?.some( - (settingPermission) => settingPermission.setting === permission.key, + const isPermissionEnabled = + settingsDraftRole.permissionFlags?.some( + (permissionFlag) => permissionFlag.flag === permission.key, ) ?? false; - const isChecked = isSettingPermissionEnabled || canUpdateAllSettings; - const isDisabled = !isEditable || canUpdateAllSettings; + const isAllSettingsOverride = + !permission.isToolPermission && + settingsDraftRole.canUpdateAllSettings === true; + const isAllToolsOverride = + permission.isToolPermission && settingsDraftRole.canAccessAllTools === true; + const isChecked = Boolean( + isPermissionEnabled || isAllSettingsOverride || isAllToolsOverride, + ); + const isDisabled = Boolean( + !isEditable || isAllSettingsOverride || isAllToolsOverride, + ); const handleChange = (value: boolean) => { - const currentPermissions = settingsDraftRole.settingPermissions ?? []; + const currentPermissions = settingsDraftRole.permissionFlags ?? []; if (value === true) { setSettingsDraftRole({ ...settingsDraftRole, - settingPermissions: [ + permissionFlags: [ ...currentPermissions, { id: v4(), - setting: permission.key, + flag: permission.key, roleId, }, ], @@ -83,8 +92,8 @@ export const SettingsRolePermissionsSettingsTableRow = ({ } else { setSettingsDraftRole({ ...settingsDraftRole, - settingPermissions: currentPermissions.filter( - (p) => p.setting !== permission.key, + permissionFlags: currentPermissions.filter( + (p) => p.flag !== permission.key, ), }); } diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsToolSection.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsToolSection.tsx new file mode 100644 index 0000000000..12e6c1d3f0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsToolSection.tsx @@ -0,0 +1,101 @@ +import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle'; +import { SettingsRolePermissionsSettingsTableHeader } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableHeader'; +import { SettingsRolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/permission-flags/components/SettingsRolePermissionsSettingsTableRow'; +import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission'; +import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { useRecoilState } from 'recoil'; + +import { H2Title, IconMail, IconTool } from 'twenty-ui/display'; +import { AnimatedExpandableContainer, Card, Section } from 'twenty-ui/layout'; +import { PermissionFlagType } from '~/generated-metadata/graphql'; + +const StyledTable = styled.div` + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; +`; + +const StyledTableRows = styled.div` + padding-bottom: ${({ theme }) => theme.spacing(2)}; + padding-top: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledCard = styled(Card)` + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +type SettingsRolePermissionsToolSectionProps = { + roleId: string; + isEditable: boolean; +}; + +export const SettingsRolePermissionsToolSection = ({ + roleId, + isEditable, +}: SettingsRolePermissionsToolSectionProps) => { + const [settingsDraftRole, setSettingsDraftRole] = useRecoilState( + settingsDraftRoleFamilyState(roleId), + ); + + const toolPermissionsConfig: SettingsRolePermissionsSettingPermission[] = [ + { + key: PermissionFlagType.SEND_EMAIL_TOOL, + name: t`Send Email`, + description: t`Allow sending emails using connected accounts`, + Icon: IconMail, + isToolPermission: true, + }, + ]; + + return ( +
+ + + { + setSettingsDraftRole({ + ...settingsDraftRole, + canAccessAllTools: !settingsDraftRole.canAccessAllTools, + }); + }} + /> + + + + + + {toolPermissionsConfig.map((permission) => ( + + ))} + + + +
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/types/SettingsRolePermissionsSettingPermission.ts b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission.ts similarity index 58% rename from packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/types/SettingsRolePermissionsSettingPermission.ts rename to packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission.ts index 8ccb79ca4f..fb1025ab6c 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/settings-permissions/types/SettingsRolePermissionsSettingPermission.ts +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission.ts @@ -1,9 +1,10 @@ import { IconComponent } from 'twenty-ui/display'; -import { SettingPermissionType } from '~/generated-metadata/graphql'; +import { PermissionFlagType } from '~/generated-metadata/graphql'; export type SettingsRolePermissionsSettingPermission = { - key: SettingPermissionType; + key: PermissionFlagType; name: string; description: string; Icon: IconComponent; + isToolPermission?: boolean; }; diff --git a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx index 15d96b8b96..6851dda311 100644 --- a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx @@ -28,7 +28,7 @@ import { useCreateOneRoleMutation, useUpdateOneRoleMutation, useUpsertObjectPermissionsMutation, - useUpsertSettingPermissionsMutation, + useUpsertPermissionFlagsMutation, } from '~/generated-metadata/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { getDirtyFields } from '~/utils/getDirtyFields'; @@ -45,6 +45,7 @@ const ROLE_BASIC_KEYS: Array = [ 'description', 'icon', 'canUpdateAllSettings', + 'canAccessAllTools', 'canReadAllObjectRecords', 'canUpdateAllObjectRecords', 'canSoftDeleteAllObjectRecords', @@ -61,7 +62,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { const [createRole] = useCreateOneRoleMutation(); const [updateRole] = useUpdateOneRoleMutation(); - const [upsertSettingPermissions] = useUpsertSettingPermissionsMutation(); + const [upsertPermissionFlags] = useUpsertPermissionFlagsMutation(); const [upsertObjectPermissions] = useUpsertObjectPermissionsMutation(); const [isSaving, setIsSaving] = useState(false); @@ -144,6 +145,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { description: settingsDraftRole.description, icon: settingsDraftRole.icon, canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings, + canAccessAllTools: settingsDraftRole.canAccessAllTools, canReadAllObjectRecords: settingsDraftRole.canReadAllObjectRecords, canUpdateAllObjectRecords: @@ -161,14 +163,14 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { return; } - if (isDefined(dirtyFields.settingPermissions)) { - await upsertSettingPermissions({ + if (isDefined(dirtyFields.permissionFlags)) { + await upsertPermissionFlags({ variables: { - upsertSettingPermissionsInput: { + upsertPermissionFlagsInput: { roleId: data.createOneRole.id, - settingPermissionKeys: - settingsDraftRole.settingPermissions?.map( - (settingPermission) => settingPermission.setting, + permissionFlagKeys: + settingsDraftRole.permissionFlags?.map( + (permissionFlag) => permissionFlag.flag, ) ?? [], }, }, @@ -214,14 +216,14 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { roleId: data.createOneRole.id, }); } else { - if (isDefined(dirtyFields.settingPermissions)) { - await upsertSettingPermissions({ + if (isDefined(dirtyFields.permissionFlags)) { + await upsertPermissionFlags({ variables: { - upsertSettingPermissionsInput: { + upsertPermissionFlagsInput: { roleId: roleId, - settingPermissionKeys: - settingsDraftRole.settingPermissions?.map( - (settingPermission) => settingPermission.setting, + permissionFlagKeys: + settingsDraftRole.permissionFlags?.map( + (permissionFlag) => permissionFlag.flag, ) ?? [], }, }, @@ -239,6 +241,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { description: settingsDraftRole.description, icon: settingsDraftRole.icon, canUpdateAllSettings: settingsDraftRole.canUpdateAllSettings, + canAccessAllTools: settingsDraftRole.canAccessAllTools, canReadAllObjectRecords: settingsDraftRole.canReadAllObjectRecords, canUpdateAllObjectRecords: diff --git a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleCreateEffect.tsx b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleCreateEffect.tsx index 0e5b689ebc..be66c6b4a2 100644 --- a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleCreateEffect.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleCreateEffect.tsx @@ -42,6 +42,7 @@ export const SettingsRoleCreateEffect = ({ description: '', icon: 'IconUser', canUpdateAllSettings: true, + canAccessAllTools: true, canReadAllObjectRecords: true, canUpdateAllObjectRecords: true, canSoftDeleteAllObjectRecords: true, diff --git a/packages/twenty-front/src/modules/settings/roles/states/settingsDraftRoleFamilyState.ts b/packages/twenty-front/src/modules/settings/roles/states/settingsDraftRoleFamilyState.ts index f6a808d3b7..4122882703 100644 --- a/packages/twenty-front/src/modules/settings/roles/states/settingsDraftRoleFamilyState.ts +++ b/packages/twenty-front/src/modules/settings/roles/states/settingsDraftRoleFamilyState.ts @@ -13,9 +13,10 @@ export const settingsDraftRoleFamilyState = createFamilyState({ canSoftDeleteAllObjectRecords: false, canUpdateAllObjectRecords: false, canUpdateAllSettings: false, + canAccessAllTools: false, isEditable: false, workspaceMembers: [], - settingPermissions: [], + permissionFlags: [], objectPermissions: [], }, }); diff --git a/packages/twenty-front/src/testing/mock-data/roles.ts b/packages/twenty-front/src/testing/mock-data/roles.ts index da750e09fd..8c2692996d 100644 --- a/packages/twenty-front/src/testing/mock-data/roles.ts +++ b/packages/twenty-front/src/testing/mock-data/roles.ts @@ -11,6 +11,7 @@ const rolesMock: Role[] = [ canSoftDeleteAllObjectRecords: true, canUpdateAllObjectRecords: true, canUpdateAllSettings: true, + canAccessAllTools: true, isEditable: false, workspaceMembers: [mockWorkspaceMembers[0]], }, @@ -23,6 +24,7 @@ const rolesMock: Role[] = [ canSoftDeleteAllObjectRecords: false, canUpdateAllObjectRecords: false, canUpdateAllSettings: false, + canAccessAllTools: false, isEditable: true, workspaceMembers: [mockWorkspaceMembers[1]], }, diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index 9023ef261d..d595b5e30d 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -3,7 +3,7 @@ import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { FeatureFlagKey, OnboardingStatus, - SettingPermissionType, + PermissionFlagType, SubscriptionInterval, SubscriptionStatus, User, @@ -125,7 +125,7 @@ export const mockedUserData: MockedUser = { workspaceMember: mockedWorkspaceMemberData, currentWorkspace: mockCurrentWorkspace, currentUserWorkspace: { - settingsPermissions: [SettingPermissionType.WORKSPACE_MEMBERS], + settingsPermissions: [PermissionFlagType.WORKSPACE_MEMBERS], objectPermissions: generatedMockObjectMetadataItems.map((item) => ({ objectMetadataId: item.id, canReadObjectRecords: true, diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1753149175945-renameSettingPermissionToPermissionFlag.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1753149175945-renameSettingPermissionToPermissionFlag.ts new file mode 100644 index 0000000000..1a9bbfcd7c --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1753149175945-renameSettingPermissionToPermissionFlag.ts @@ -0,0 +1,46 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameSettingPermissionToPermissionFlag1753149175945 + implements MigrationInterface +{ + name = 'RenameSettingPermissionToPermissionFlag1753149175945'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."settingPermission" RENAME TO "permissionFlag"`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" RENAME COLUMN "setting" TO "flag"`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" RENAME CONSTRAINT "IDX_SETTING_PERMISSION_SETTING_ROLE_ID_UNIQUE" TO "IDX_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE"`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" DROP CONSTRAINT "FK_b327aadd9fd189f33d2c5237833"`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" ADD CONSTRAINT "FK_13f8ca9c517976733a1ce4c10eb" FOREIGN KEY ("roleId") REFERENCES "core"."role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" RENAME COLUMN "flag" TO "setting"`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" RENAME TO "settingPermission"`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" DROP CONSTRAINT "FK_13f8ca9c517976733a1ce4c10eb"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."permissionFlag" ADD CONSTRAINT "FK_b327aadd9fd189f33d2c5237833" FOREIGN KEY ("roleId") REFERENCES "core"."role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1753318977613-AddCanAccessAllToolsColumnToRole.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1753318977613-AddCanAccessAllToolsColumnToRole.ts new file mode 100644 index 0000000000..074658c57f --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1753318977613-AddCanAccessAllToolsColumnToRole.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddCanAccessAllToolsColumnToRole1753318977613 + implements MigrationInterface +{ + name = 'AddCanAccessAllToolsColumnToRole1753318977613'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."role" ADD "canAccessAllTools" boolean NOT NULL DEFAULT false`, + ); + + await queryRunner.query( + `UPDATE "core"."role" SET "canAccessAllTools" = true WHERE "label" = 'Admin'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."role" DROP COLUMN "canAccessAllTools"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements.ts index 603fad32d5..b709deaeb5 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements.ts @@ -1,6 +1,6 @@ -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; export const OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS = { - apiKey: SettingPermissionType.API_KEYS_AND_WEBHOOKS, - webhook: SettingPermissionType.API_KEYS_AND_WEBHOOKS, + apiKey: PermissionFlagType.API_KEYS_AND_WEBHOOKS, + webhook: PermissionFlagType.API_KEYS_AND_WEBHOOKS, } as const; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts index 27aaab4d31..8668c85dec 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts @@ -26,7 +26,7 @@ import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-quer import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -191,7 +191,7 @@ export abstract class GraphqlQueryBaseResolverService< objectMetadataItemWithFieldMaps.nameSingular, ) ) { - const permissionRequired: SettingPermissionType = + const permissionRequired: PermissionFlagType = // @ts-expect-error legacy noImplicitAny OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS[ objectMetadataItemWithFieldMaps.nameSingular diff --git a/packages/twenty-server/src/engine/core-modules/ai/ai.module.ts b/packages/twenty-server/src/engine/core-modules/ai/ai.module.ts index 0305e08c49..de82ba5ccf 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/ai.module.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/ai.module.ts @@ -11,11 +11,16 @@ import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-ada import { ToolService } from 'src/engine/core-modules/ai/services/tool.service'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service'; +import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { MessagingModule } from 'src/modules/messaging/messaging.module'; @Global() @Module({ @@ -27,6 +32,9 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ WorkspacePermissionsCacheModule, WorkspaceCacheStorageModule, UserRoleModule, + TwentyORMModule, + MessagingModule, + PermissionsModule, ], controllers: [AiController, McpController], providers: [ @@ -34,8 +42,10 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ AiModelRegistryService, ToolService, ToolAdapterService, + ToolRegistryService, AIBillingService, McpService, + SendEmailTool, ], exports: [ AiService, @@ -43,7 +53,9 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ AIBillingService, ToolService, ToolAdapterService, + ToolRegistryService, McpService, + SendEmailTool, ], }) export class AiModule {} diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts index 3ebbe182a5..6a35599378 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/tool-adapter.service.ts @@ -2,28 +2,49 @@ import { Injectable } from '@nestjs/common'; import { ToolSet } from 'ai'; -import { TOOLS } from 'src/engine/core-modules/tool/constants/tools.const'; +import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service'; +import { ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; +import { Tool } from 'src/engine/core-modules/tool/types/tool.type'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; +import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; @Injectable() export class ToolAdapterService { - getCoreTools(): ToolSet { - const tools = Array.from(TOOLS.entries()).reduce( - (acc, [toolType, tool]) => { - acc[toolType.toLowerCase()] = { - description: tool.description, - parameters: tool.parameters, - execute: async (parameters) => { - const response = await tool.execute(parameters.input); + constructor( + private readonly toolRegistry: ToolRegistryService, + private readonly permissionsService: PermissionsService, + ) {} - return response; - }, - }; + async getTools(roleId?: string, workspaceId?: string): Promise { + const tools: ToolSet = {}; - return acc; - }, - {}, - ); + for (const toolType of this.toolRegistry.getAllToolTypes()) { + const tool = this.toolRegistry.getTool(toolType); + + if (!tool.flag) { + tools[toolType.toLowerCase()] = this.createToolSet(tool); + } else if (roleId && workspaceId) { + const hasPermission = await this.permissionsService.hasToolPermission( + roleId, + workspaceId, + tool.flag as PermissionFlagType, + ); + + if (hasPermission) { + tools[toolType.toLowerCase()] = this.createToolSet(tool); + } + } + } return tools; } + + private createToolSet(tool: Tool) { + return { + description: tool.description, + parameters: tool.parameters, + execute: async (parameters: { input: ToolInput }) => + tool.execute(parameters.input), + }; + } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 7b835ccfb7..3467cbc89f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -65,7 +65,7 @@ import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { TwoFactorAuthenticationExceptionFilter } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication-exception.filter'; @@ -548,7 +548,7 @@ export class AuthResolver { @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.API_KEYS_AND_WEBHOOKS), + SettingsPermissionsGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS), ) @Mutation(() => ApiKeyToken) async generateApiKeyToken( diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts index 4a6adaca03..703b30cfa0 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts @@ -32,7 +32,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -60,7 +60,7 @@ export class BillingResolver { @Query(() => BillingSessionOutput) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), + SettingsPermissionsGuard(PermissionFlagType.WORKSPACE), ) async billingPortalSession( @AuthWorkspace() workspace: Workspace, @@ -124,7 +124,7 @@ export class BillingResolver { @Mutation(() => BillingUpdateOutput) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), + SettingsPermissionsGuard(PermissionFlagType.WORKSPACE), ) async switchToYearlyInterval(@AuthWorkspace() workspace: Workspace) { await this.billingSubscriptionService.switchToYearlyInterval(workspace); @@ -135,7 +135,7 @@ export class BillingResolver { @Mutation(() => BillingUpdateOutput) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), + SettingsPermissionsGuard(PermissionFlagType.WORKSPACE), ) async switchToEnterprisePlan(@AuthWorkspace() workspace: Workspace) { await this.billingSubscriptionService.switchToEnterprisePlan(workspace); @@ -154,7 +154,7 @@ export class BillingResolver { @Mutation(() => BillingEndTrialPeriodOutput) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), + SettingsPermissionsGuard(PermissionFlagType.WORKSPACE), ) async endSubscriptionTrialPeriod( @AuthWorkspace() workspace: Workspace, @@ -165,7 +165,7 @@ export class BillingResolver { @Query(() => [BillingMeteredProductUsageOutput]) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), + SettingsPermissionsGuard(PermissionFlagType.WORKSPACE), ) async getMeteredProductsUsage( @AuthWorkspace() workspace: Workspace, @@ -199,7 +199,7 @@ export class BillingResolver { await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId, workspaceId, - setting: SettingPermissionType.WORKSPACE, + setting: PermissionFlagType.WORKSPACE, isExecutedByApiKey, }); diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts index a9c91e20a7..1cf1580271 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts @@ -23,7 +23,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services/imap-smtp-caldav-apis.service'; @@ -32,7 +32,7 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/s @Resolver() @UsePipes(ResolverValidationPipe) @UseFilters(AuthGraphqlApiExceptionFilter, PermissionsGraphqlApiExceptionFilter) -@UseGuards(SettingsPermissionsGuard(SettingPermissionType.WORKSPACE)) +@UseGuards(SettingsPermissionsGuard(PermissionFlagType.WORKSPACE)) export class ImapSmtpCaldavResolver { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, diff --git a/packages/twenty-server/src/engine/core-modules/lab/lab.resolver.ts b/packages/twenty-server/src/engine/core-modules/lab/lab.resolver.ts index e1ac254cae..e9fdde506d 100644 --- a/packages/twenty-server/src/engine/core-modules/lab/lab.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/lab/lab.resolver.ts @@ -13,7 +13,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; @Resolver() @@ -23,7 +23,7 @@ import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-module PermissionsGraphqlApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter, ) -@UseGuards(SettingsPermissionsGuard(SettingPermissionType.WORKSPACE)) +@UseGuards(SettingsPermissionsGuard(PermissionFlagType.WORKSPACE)) export class LabResolver { constructor(private featureFlagService: FeatureFlagService) {} diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts index 08760a7c93..b71aaca3d0 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts @@ -22,7 +22,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; @Resolver() @@ -31,7 +31,7 @@ import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-module PreventNestToAutoLogGraphqlErrorsFilter, ) @UsePipes(ResolverValidationPipe) -@UseGuards(SettingsPermissionsGuard(SettingPermissionType.SECURITY)) +@UseGuards(SettingsPermissionsGuard(PermissionFlagType.SECURITY)) export class SSOResolver { constructor(private readonly sSOService: SSOService) {} diff --git a/packages/twenty-server/src/engine/core-modules/tool/constants/tools.const.ts b/packages/twenty-server/src/engine/core-modules/tool/constants/tools.const.ts deleted file mode 100644 index 32132f999b..0000000000 --- a/packages/twenty-server/src/engine/core-modules/tool/constants/tools.const.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum'; -import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool'; -import { Tool } from 'src/engine/core-modules/tool/types/tool.type'; - -export const TOOLS: Map = new Map([ - [ToolType.HTTP_REQUEST, new HttpTool()], -]); diff --git a/packages/twenty-server/src/engine/core-modules/tool/enums/tool-type.enum.ts b/packages/twenty-server/src/engine/core-modules/tool/enums/tool-type.enum.ts index 3e75af4aad..2c7e8c738e 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/enums/tool-type.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/enums/tool-type.enum.ts @@ -1,3 +1,4 @@ export enum ToolType { HTTP_REQUEST = 'HTTP_REQUEST', + SEND_EMAIL = 'SEND_EMAIL', } diff --git a/packages/twenty-server/src/engine/core-modules/tool/services/tool-registry.service.ts b/packages/twenty-server/src/engine/core-modules/tool/services/tool-registry.service.ts new file mode 100644 index 0000000000..c5c694acbe --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool/services/tool-registry.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; + +import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum'; +import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool'; +import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool'; +import { SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type'; +import { Tool } from 'src/engine/core-modules/tool/types/tool.type'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; + +@Injectable() +export class ToolRegistryService { + private readonly toolFactories: Map Tool>; + + constructor(private readonly sendEmailTool: SendEmailTool) { + this.toolFactories = new Map Tool>([ + [ToolType.HTTP_REQUEST, () => new HttpTool()], + [ + ToolType.SEND_EMAIL, + () => ({ + description: this.sendEmailTool.description, + parameters: this.sendEmailTool.parameters, + execute: (params) => + this.sendEmailTool.execute(params as SendEmailInput), + flag: PermissionFlagType.SEND_EMAIL_TOOL, + }), + ], + ]); + } + + getTool(toolType: ToolType): Tool { + const factory = this.toolFactories.get(toolType); + + if (!factory) { + throw new Error(`Unknown tool type: ${toolType}`); + } + + return factory(); + } + + getAllToolTypes(): ToolType[] { + return Array.from(this.toolFactories.keys()); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts new file mode 100644 index 0000000000..75824fcca9 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts @@ -0,0 +1,14 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class SendEmailToolException extends CustomException { + constructor(message: string, code: SendEmailToolExceptionCode) { + super(message, code); + } +} + +export enum SendEmailToolExceptionCode { + INVALID_CONNECTED_ACCOUNT_ID = 'INVALID_CONNECTED_ACCOUNT_ID', + CONNECTED_ACCOUNT_NOT_FOUND = 'CONNECTED_ACCOUNT_NOT_FOUND', + INVALID_EMAIL = 'INVALID_EMAIL', + WORKSPACE_ID_NOT_FOUND = 'WORKSPACE_ID_NOT_FOUND', +} diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts new file mode 100644 index 0000000000..f2ff90eafa --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +export const SendEmailInputZodSchema = z.object({ + email: z.string().email().describe('The recipient email address'), + subject: z.string().describe('The email subject line'), + body: z.string().describe('The email body content (HTML or plain text)'), + connectedAccountId: z + .string() + .uuid() + .describe( + 'The UUID of the connected account to send the email from. Provide this only if you have it; otherwise, leave blank.', + ) + .optional(), +}); + +export const SendEmailToolParametersZodSchema = z.object({ + toolDescription: z + .string() + .describe( + "A clear, human-readable status message describing the email being sent. This will be shown to the user while the tool is being called, so phrase it as a present-tense status update (e.g., 'Sending email to customer about order status'). Explain what email you are sending and to whom in natural language.", + ), + input: SendEmailInputZodSchema, +}); diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts new file mode 100644 index 0000000000..ac8f52cf05 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts @@ -0,0 +1,155 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import DOMPurify from 'dompurify'; +import { JSDOM } from 'jsdom'; +import { isDefined, isValidUuid } from 'twenty-shared/utils'; +import { z } from 'zod'; + +import { + SendEmailToolException, + SendEmailToolExceptionCode, +} from 'src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception'; +import { SendEmailToolParametersZodSchema } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema'; +import { SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type'; +import { ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { Tool } from 'src/engine/core-modules/tool/types/tool.type'; +import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service'; + +@Injectable() +export class SendEmailTool implements Tool { + private readonly logger = new Logger(SendEmailTool.name); + + description = + 'Send an email using a connected account. Requires SEND_EMAIL_TOOL permission.'; + parameters = SendEmailToolParametersZodSchema; + + constructor( + private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly sendMessageService: MessagingSendMessageService, + ) {} + + private async getConnectedAccount( + connectedAccountId: string, + workspaceId: string, + ) { + if (!isValidUuid(connectedAccountId)) { + throw new SendEmailToolException( + `Connected Account ID is not a valid UUID`, + SendEmailToolExceptionCode.INVALID_CONNECTED_ACCOUNT_ID, + ); + } + + const connectedAccountRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'connectedAccount', + ); + const connectedAccount = await connectedAccountRepository.findOneBy({ + id: connectedAccountId, + }); + + if (!isDefined(connectedAccount)) { + throw new SendEmailToolException( + `Connected Account '${connectedAccountId}' not found`, + SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, + ); + } + + return connectedAccount; + } + + private async getOrThrowFirstConnectedAccountId( + workspaceId: string, + ): Promise { + const connectedAccountRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'connectedAccount', + ); + const allAccounts = await connectedAccountRepository.find(); + + if (!allAccounts || allAccounts.length === 0) { + throw new SendEmailToolException( + 'No connected accounts found for this workspace', + SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, + ); + } + + return allAccounts[0].id; + } + + async execute(parameters: SendEmailInput): Promise { + const { workspaceId } = this.scopedWorkspaceContextFactory.create(); + + const { email, subject, body } = parameters; + let { connectedAccountId } = parameters; + + try { + const emailSchema = z.string().trim().email('Invalid email'); + const emailValidation = emailSchema.safeParse(email); + + if (!emailValidation.success) { + throw new SendEmailToolException( + `Email '${email}' is invalid`, + SendEmailToolExceptionCode.INVALID_EMAIL, + ); + } + + if (!workspaceId) { + throw new SendEmailToolException( + 'Workspace ID not found', + SendEmailToolExceptionCode.WORKSPACE_ID_NOT_FOUND, + ); + } + + if (!connectedAccountId) { + connectedAccountId = + await this.getOrThrowFirstConnectedAccountId(workspaceId); + } + + const connectedAccount = await this.getConnectedAccount( + connectedAccountId, + workspaceId, + ); + + const window = new JSDOM('').window; + const purify = DOMPurify(window); + const safeBody = purify.sanitize(body || ''); + const safeSubject = purify.sanitize(subject || ''); + + await this.sendMessageService.sendMessage( + { + to: email, + subject: safeSubject, + body: safeBody, + }, + connectedAccount, + ); + + this.logger.log(`Email sent successfully to ${email}`); + + return { + result: { + success: true, + message: `Email sent successfully to ${email}`, + }, + }; + } catch (error) { + if (error instanceof SendEmailToolException) { + return { + error: error.message, + }; + } + + this.logger.error(`Failed to send email: ${error}`); + + return { + error: error instanceof Error ? error.message : 'Failed to send email', + }; + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type.ts new file mode 100644 index 0000000000..dc250a29a5 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +import { SendEmailInputZodSchema } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.schema'; + +export type SendEmailInput = z.infer; diff --git a/packages/twenty-server/src/engine/core-modules/tool/types/tool.type.ts b/packages/twenty-server/src/engine/core-modules/tool/types/tool.type.ts index 42c1239a9f..0cf8ab45da 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/types/tool.type.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/types/tool.type.ts @@ -3,9 +3,11 @@ import { ZodType } from 'zod'; import { ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; import { ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; export type Tool = { description: string; parameters: JSONSchema7 | ZodType; execute(input: ToolInput): Promise; + flag?: PermissionFlagType; }; diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts index 2a3a3d9f5e..a9b0f63e04 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts @@ -19,15 +19,15 @@ import { } from 'typeorm'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { TwoFactorAuthenticationMethodSummaryDto } from 'src/engine/core-modules/two-factor-authentication/dto/two-factor-authentication-method.dto'; +import { TwoFactorAuthenticationMethod } from 'src/engine/core-modules/two-factor-authentication/entities/two-factor-authentication-method.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; -import { TwoFactorAuthenticationMethod } from 'src/engine/core-modules/two-factor-authentication/entities/two-factor-authentication-method.entity'; -import { TwoFactorAuthenticationMethodSummaryDto } from 'src/engine/core-modules/two-factor-authentication/dto/two-factor-authentication-method.dto'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; -registerEnumType(SettingPermissionType, { - name: 'SettingPermissionType', +registerEnumType(PermissionFlagType, { + name: 'PermissionFlagType', }); registerEnumType(PermissionsOnAllObjectRecords, { @@ -96,8 +96,8 @@ export class UserWorkspace { ) twoFactorAuthenticationMethods: Relation; - @Field(() => [SettingPermissionType], { nullable: true }) - settingsPermissions?: SettingPermissionType[]; + @Field(() => [PermissionFlagType], { nullable: true }) + settingsPermissions?: PermissionFlagType[]; @Field(() => [PermissionsOnAllObjectRecords], { nullable: true, diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts index 7dbe1d99c9..a2cfee8691 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts @@ -12,7 +12,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; @@ -21,7 +21,7 @@ import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-bu @UseGuards( WorkspaceAuthGuard, UserAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS), + SettingsPermissionsGuard(PermissionFlagType.WORKFLOWS), ) @UsePipes(ResolverValidationPipe) @UseFilters( diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-step.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-step.resolver.ts index 2cf2708e5a..c1bd40d8aa 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-step.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-step.resolver.ts @@ -16,7 +16,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service'; import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; @@ -27,7 +27,7 @@ import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runne @UseGuards( WorkspaceAuthGuard, UserAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS), + SettingsPermissionsGuard(PermissionFlagType.WORKFLOWS), ) @UseFilters( PermissionsGraphqlApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts index 9825f66691..813502bfc8 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts @@ -14,7 +14,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service'; @@ -24,7 +24,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @UseGuards( WorkspaceAuthGuard, UserAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS), + SettingsPermissionsGuard(PermissionFlagType.WORKFLOWS), ) @UsePipes(ResolverValidationPipe) @UseFilters( diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts index e6e304d4de..50b2e48ffd 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts @@ -10,7 +10,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service'; @@ -19,7 +19,7 @@ import { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-b @UseGuards( WorkspaceAuthGuard, UserAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS), + SettingsPermissionsGuard(PermissionFlagType.WORKFLOWS), ) @UseFilters( PermissionsGraphqlApiExceptionFilter, diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts index 5958767ea3..dfadf84796 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts @@ -14,7 +14,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -23,7 +23,7 @@ import { SendInvitationsInput } from './dtos/send-invitations.input'; @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKSPACE_MEMBERS), + SettingsPermissionsGuard(PermissionFlagType.WORKSPACE_MEMBERS), ) @UsePipes(ResolverValidationPipe) @UseFilters( diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index fd649cf344..23a2e4ec8b 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -36,7 +36,7 @@ import { WorkspaceExceptionCode, } from 'src/engine/core-modules/workspace/workspace.exception'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -457,7 +457,7 @@ export class WorkspaceService extends TypeOrmQueryService { const userHasPermission = await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId, - setting: SettingPermissionType.SECURITY, + setting: PermissionFlagType.SECURITY, workspaceId: workspaceId, isExecutedByApiKey: isDefined(apiKey), }); @@ -504,7 +504,7 @@ export class WorkspaceService extends TypeOrmQueryService { await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId, workspaceId, - setting: SettingPermissionType.WORKSPACE, + setting: PermissionFlagType.WORKSPACE, isExecutedByApiKey: isDefined(apiKey), }); diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index 55b88eb67f..ce2e6f9659 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -58,7 +58,7 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { AgentService } from 'src/engine/metadata-modules/agent/agent.service'; import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; @@ -144,7 +144,7 @@ export class WorkspaceResolver { @Mutation(() => SignedFileDTO) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), + SettingsPermissionsGuard(PermissionFlagType.WORKSPACE), ) async uploadWorkspaceLogo( @AuthWorkspace() { id }: Workspace, @@ -190,7 +190,7 @@ export class WorkspaceResolver { @Mutation(() => Workspace) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.WORKSPACE), + SettingsPermissionsGuard(PermissionFlagType.WORKSPACE), ) async deleteCurrentWorkspace(@AuthWorkspace() { id }: Workspace) { return this.workspaceService.deleteWorkspace(id); diff --git a/packages/twenty-server/src/engine/guards/settings-permissions.guard.ts b/packages/twenty-server/src/engine/guards/settings-permissions.guard.ts index 42d9e58dad..de000810d3 100644 --- a/packages/twenty-server/src/engine/guards/settings-permissions.guard.ts +++ b/packages/twenty-server/src/engine/guards/settings-permissions.guard.ts @@ -10,7 +10,7 @@ import { GqlExecutionContext } from '@nestjs/graphql'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -19,7 +19,7 @@ import { import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; export const SettingsPermissionsGuard = ( - requiredPermission: SettingPermissionType, + requiredPermission: PermissionFlagType, ): Type => { @Injectable() class SettingsPermissionsMixin implements CanActivate { diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool.service.ts index 0ce793f2d9..d98675553c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool.service.ts @@ -26,9 +26,9 @@ export class AgentToolService { try { const agent = await this.agentService.findOneAgent(agentId, workspaceId); - const actionTools = this.toolAdapterService.getCoreTools(); - if (!agent.roleId) { + const actionTools = await this.toolAdapterService.getTools(); + return actionTools; } @@ -43,6 +43,11 @@ export class AgentToolService { return {}; } + const actionTools = await this.toolAdapterService.getTools( + role.id, + workspaceId, + ); + const databaseTools = await this.toolService.listTools( role.id, workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-system-prompts.const.ts b/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-system-prompts.const.ts index 624aecb167..58ee569f64 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-system-prompts.const.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/constants/agent-system-prompts.const.ts @@ -1,63 +1,24 @@ export const AGENT_SYSTEM_PROMPTS = { - AGENT_EXECUTION: `You are an AI agent node in a workflow builder system with access to comprehensive database operations and the ability to make HTTP requests. Your role is to process inputs, execute actions using available tools, and provide structured outputs that can be used by subsequent workflow nodes. + AGENT_EXECUTION: `You are an AI agent with access to various tools that will be provided to you dynamically. The available tools and their descriptions are passed to you through the tools property, so you should only use tools that are actually available to you. -AVAILABLE TOOLS: -You have access to: -- DATABASE OPERATIONS: Full CRUD operations for all standard objects in the system (see below) -- HTTP REQUESTS: Use the http_request tool to make HTTP calls to external APIs or services - -DATABASE OPERATIONS: -- CREATE: create_[object] - Create new records (e.g., create_person, create_company, create_opportunity) -- READ: find_[object] and find_one_[object] - Search and retrieve records -- UPDATE: update_[object] - Modify existing records -- DELETE: soft_delete_[object] and destroy_[object] - Remove records (soft or permanent) - -Common objects include: person, company, opportunity, task, note etc. and any custom objects. - -HTTP REQUEST TOOL: -- Use the http_request tool when the user asks you to call an external API, fetch data from a web service, or interact with a remote endpoint. -- You must provide a clear toolDescription and specify the input (url, method, headers, body) as required by the tool schema. -- Only use the http_request tool for actual HTTP/API calls. Do not simulate or describe them if the tool is not available. -- Always verify tool results and handle errors appropriately. If an HTTP request fails, explain the issue and suggest alternatives if possible. +TOOL USAGE GUIDELINES (applies to all tools): +- Only use a tool if it is available and you have permission. +- Always verify tool results and handle errors appropriately. +- If a tool operation fails, explain the issue and suggest alternatives. +- If you lack permission for a tool, respond: "I cannot perform this operation because I don't have the necessary permissions. Please check that I have been assigned the appropriate role for this workspace." Your responsibilities: 1. Analyze the input context and prompt carefully -2. If the request involves database operations (create, read, update, delete), check if you have the required tools available -3. If the request involves making an HTTP request, check if the http_request tool is available -4. If a requested tool is NOT available, state that you lack permissions for that specific operation. You can respond with: - "I cannot perform this operation because I don't have the necessary permissions. Please check that I have been assigned the appropriate role for this workspace." -5. If tools ARE available, use them to perform the requested operations -6. If no tool operations are needed, process the request directly with your analysis -7. Provide comprehensive responses that include all relevant information and context +2. If a requested tool is not available, state the limitation as above +3. If no tool operations are needed, process the request directly +4. Provide comprehensive, structured responses for workflow consumption Workflow context: - - You are part of a larger workflow system where your output may be used by other nodes - - Maintain consistency and reliability in your responses - - Consider the broader workflow context when making decisions - - If you encounter data or perform actions, document them clearly in your response +- You are part of a larger workflow system; your output may be used by other nodes +- Maintain consistency and reliability in your responses +- Document any data or actions clearly -Tool usage guidelines: - - Use tools for database operations or HTTP requests when requested - do not simulate or describe them - - Use create_[object] tools when asked to create new records - - Use find_[object] tools when asked to search or retrieve records - - Use update_[object] tools when asked to modify existing records - - Use soft_delete_[object] or destroy_[object] when asked to remove records - - Use the http_request tool when asked to call an external API or perform an HTTP operation - - Always verify tool results and handle errors appropriately - - Provide context about what tools you used and why - - If a tool fails, explain the issue and suggest alternatives - -Permission handling: - - Only check for permissions when tool operations are actually requested - - If you don't have the necessary tools for a requested operation, clearly state the limitation - - For non-tool requests, proceed normally without permission checks - -Important: After your response, the system will call generateObject to convert your output into a structured format according to a specific schema. Therefore: - - Provide comprehensive information in your response - - Include all relevant data you've gathered or processed - - Structure your response logically so it can be easily parsed - - Mention any important context, decisions, or actions taken - - Include tool execution results in your response`, +Important: After your response, the system will call generateObject to convert your output into a structured format. Ensure your response is comprehensive, logically structured, and includes all relevant data and tool results.`, OUTPUT_GENERATOR: `You are a structured output generator for a workflow system. Your role is to convert the provided execution results into a structured format according to a specific schema. @@ -84,14 +45,14 @@ Guidelines: - Provide insights, support, and updates about people, companies, opportunities, tasks, notes, and other business objects. - Access and summarize information you have permission to see - Help users understand how to use the system and its features -- Make HTTP requests to external APIs or services using the http_request tool when asked +- Use various tools that are provided to you dynamically when needed Permissions and capabilities: - You can only perform actions and access data that your assigned role and permissions allow - If a user requests something you do not have permission for, politely explain the limitation (e.g., "I cannot perform this operation because I don't have the necessary permissions. Please check your role or contact an admin.") - If you are unsure about your permissions for a specific action, ask the user for clarification or suggest they check with an administrator - Do not attempt to simulate or fake actions you cannot perform -- If you do not have access to the http_request tool, explain that you cannot make HTTP requests +- Only use tools that are actually available to you through the tools property If you need more information to answer a question, ask follow-up questions. Always be transparent about your capabilities and limitations. diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts index ebad2db198..9b1021c421 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts @@ -41,7 +41,7 @@ import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util'; import { fromFieldMetadataEntityToFieldMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-field-metadata-dto.util'; import { fromObjectMetadataEntityToObjectMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-object-metadata-entity-to-object-metadata-dto.util'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { isMorphRelationFieldMetadataType } from 'src/engine/utils/is-morph-relation-field-metadata-type.util'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; @@ -59,7 +59,7 @@ export class FieldMetadataResolver { private readonly beforeUpdateOneField: BeforeUpdateOneField, ) {} - @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(PermissionFlagType.DATA_MODEL)) @Mutation(() => FieldMetadataDTO) async createOneField( @Args('input') input: CreateOneFieldMetadataInput, @@ -75,7 +75,7 @@ export class FieldMetadataResolver { } } - @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(PermissionFlagType.DATA_MODEL)) @Mutation(() => FieldMetadataDTO) async updateOneField( @Args('input') input: UpdateOneFieldMetadataInput, @@ -97,7 +97,7 @@ export class FieldMetadataResolver { } } - @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(PermissionFlagType.DATA_MODEL)) @Mutation(() => FieldMetadataDTO) async deleteOneField( @Args('input') input: DeleteOneFieldInput, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index 0cb9ab9e5b..b2102d4fa0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -21,7 +21,7 @@ import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metad import { ObjectMetadataFieldRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service'; import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service'; import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module'; @@ -83,9 +83,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; }, create: { many: { disabled: true }, - guards: [ - SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL), - ], + guards: [SettingsPermissionsGuard(PermissionFlagType.DATA_MODEL)], }, update: { disabled: true }, delete: { disabled: true }, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts index a9f6157b8d..7cc575bc9f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts @@ -28,7 +28,7 @@ import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metada import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util'; import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; @UseGuards(WorkspaceAuthGuard) @@ -80,7 +80,7 @@ export class ObjectMetadataResolver { ); } - @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(PermissionFlagType.DATA_MODEL)) @ResolveField(() => String, { nullable: true }) async icon( @Parent() objectMetadata: ObjectMetadataDTO, @@ -93,7 +93,7 @@ export class ObjectMetadataResolver { ); } - @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(PermissionFlagType.DATA_MODEL)) @Mutation(() => ObjectMetadataDTO) async deleteOneObject( @Args('input') input: DeleteOneObjectInput, @@ -109,7 +109,7 @@ export class ObjectMetadataResolver { } } - @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) + @UseGuards(SettingsPermissionsGuard(PermissionFlagType.DATA_MODEL)) @Mutation(() => ObjectMetadataDTO) async updateOneObject( @Args('input') input: UpdateOneObjectInput, diff --git a/packages/twenty-server/src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto.ts new file mode 100644 index 0000000000..592f18c6eb --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto.ts @@ -0,0 +1,15 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; + +@ObjectType('PermissionFlag') +export class PermissionFlagDTO { + @Field({ nullable: false }) + id: string; + + @Field({ nullable: false }) + roleId: string; + + @Field({ nullable: false }) + flag: PermissionFlagType; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input.ts new file mode 100644 index 0000000000..f84b8b3d96 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input.ts @@ -0,0 +1,18 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { IsArray, IsEnum, IsNotEmpty, IsUUID } from 'class-validator'; + +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; + +@InputType() +export class UpsertPermissionFlagsInput { + @IsUUID() + @IsNotEmpty() + @Field() + roleId: string; + + @IsArray() + @IsEnum(PermissionFlagType, { each: true }) + @Field(() => [PermissionFlagType]) + permissionFlagKeys: PermissionFlagType[]; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.entity.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.entity.ts similarity index 64% rename from packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.entity.ts rename to packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.entity.ts index dd135b77f0..8619798b7a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.entity.ts @@ -10,26 +10,26 @@ import { UpdateDateColumn, } from 'typeorm'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -@Entity('settingPermission') -@Unique('IDX_SETTING_PERMISSION_SETTING_ROLE_ID_UNIQUE', ['setting', 'roleId']) -export class SettingPermissionEntity { +@Entity('permissionFlag') +@Unique('IDX_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE', ['flag', 'roleId']) +export class PermissionFlagEntity { @PrimaryGeneratedColumn('uuid') id: string; @Column({ nullable: false, type: 'uuid' }) roleId: string; - @ManyToOne(() => RoleEntity, (role) => role.settingPermissions, { + @ManyToOne(() => RoleEntity, (role) => role.permissionFlags, { onDelete: 'CASCADE', }) @JoinColumn({ name: 'roleId' }) role: Relation; @Column({ nullable: false, type: 'varchar' }) - setting: SettingPermissionType; + flag: PermissionFlagType; @Column({ nullable: false, type: 'uuid' }) workspaceId: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts new file mode 100644 index 0000000000..9b8be9e047 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; +import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([PermissionFlagEntity, RoleEntity], 'core'), + WorkspacePermissionsCacheModule, + ], + + providers: [PermissionFlagService], + exports: [PermissionFlagService], +}) +export class PermissionFlagModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.service.ts similarity index 65% rename from packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.service.ts rename to packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.service.ts index 4d995cfcab..2acb01a5a4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.service.ts @@ -3,21 +3,21 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'twenty-shared/utils'; import { DataSource, In, Repository } from 'typeorm'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input'; +import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsException, PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { UpsertSettingPermissionsInput } from 'src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input'; -import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; -export class SettingPermissionService { +export class PermissionFlagService { constructor( - @InjectRepository(SettingPermissionEntity, 'core') - private readonly settingPermissionRepository: Repository, + @InjectRepository(PermissionFlagEntity, 'core') + private readonly permissionFlagRepository: Repository, @InjectRepository(RoleEntity, 'core') private readonly roleRepository: Repository, @InjectDataSource('core') @@ -25,25 +25,25 @@ export class SettingPermissionService { private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, ) {} - public async upsertSettingPermissions({ + public async upsertPermissionFlags({ workspaceId, input, }: { workspaceId: string; - input: UpsertSettingPermissionsInput; - }): Promise { + input: UpsertPermissionFlagsInput; + }): Promise { await this.validateRoleIsEditableOrThrow({ roleId: input.roleId, workspaceId, }); - const invalidSettings = input.settingPermissionKeys.filter( - (setting) => !Object.values(SettingPermissionType).includes(setting), + const invalidFlags = input.permissionFlagKeys.filter( + (flag) => !Object.values(PermissionFlagType).includes(flag), ); - if (invalidSettings.length > 0) { + if (invalidFlags.length > 0) { throw new PermissionsException( - `${PermissionsExceptionMessage.INVALID_SETTING}: ${invalidSettings.join(', ')}`, + `${PermissionsExceptionMessage.INVALID_SETTING}: ${invalidFlags.join(', ')}`, PermissionsExceptionCode.INVALID_SETTING, ); } @@ -55,7 +55,7 @@ export class SettingPermissionService { try { const existingPermissions = await queryRunner.manager.find( - SettingPermissionEntity, + PermissionFlagEntity, { where: { roleId: input.roleId, @@ -63,41 +63,39 @@ export class SettingPermissionService { }, }, ); - const existingSettings = new Set( - existingPermissions.map((p) => p.setting), - ); - const inputSettings = new Set(input.settingPermissionKeys); + const existingSettings = new Set(existingPermissions.map((p) => p.flag)); + const inputSettings = new Set(input.permissionFlagKeys); - const settingsToAdd = input.settingPermissionKeys.filter( + const flagsToAdd = input.permissionFlagKeys.filter( (setting) => !existingSettings.has(setting), ); const permissionsToRemove = existingPermissions.filter( - (permission) => !inputSettings.has(permission.setting), + (permission) => !inputSettings.has(permission.flag), ); if (permissionsToRemove.length > 0) { - await queryRunner.manager.delete(SettingPermissionEntity, { + await queryRunner.manager.delete(PermissionFlagEntity, { id: In(permissionsToRemove.map((p) => p.id)), }); } - if (settingsToAdd.length > 0) { - const newPermissions = settingsToAdd.map((setting) => - queryRunner.manager.create(SettingPermissionEntity, { + if (flagsToAdd.length > 0) { + const newPermissions = flagsToAdd.map((flag) => + queryRunner.manager.create(PermissionFlagEntity, { workspaceId, roleId: input.roleId, - setting, + flag, }), ); - await queryRunner.manager.save(SettingPermissionEntity, newPermissions); + await queryRunner.manager.save(PermissionFlagEntity, newPermissions); } await queryRunner.commitTransaction(); - return queryRunner.manager.find(SettingPermissionEntity, { + return queryRunner.manager.find(PermissionFlagEntity, { where: { roleId: input.roleId, workspaceId }, - order: { setting: 'ASC' }, + order: { flag: 'ASC' }, }); } catch (error) { await queryRunner.rollbackTransaction(); diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/constants/setting-permission-type.constants.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/constants/permission-flag-type.constants.ts similarity index 67% rename from packages/twenty-server/src/engine/metadata-modules/permissions/constants/setting-permission-type.constants.ts rename to packages/twenty-server/src/engine/metadata-modules/permissions/constants/permission-flag-type.constants.ts index 2368d86755..128d2245ed 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/constants/setting-permission-type.constants.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/constants/permission-flag-type.constants.ts @@ -1,4 +1,5 @@ -export enum SettingPermissionType { +export enum PermissionFlagType { + // Settings permissions API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS', WORKSPACE = 'WORKSPACE', WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS', @@ -7,4 +8,7 @@ export enum SettingPermissionType { ADMIN_PANEL = 'ADMIN_PANEL', SECURITY = 'SECURITY', WORKFLOWS = 'WORKFLOWS', + + // Tool permissions + SEND_EMAIL_TOOL = 'SEND_EMAIL_TOOL', } diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts index 58e8d88147..f408ca0f91 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts @@ -1,19 +1,22 @@ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsException, PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { UserWorkspacePermissions } from 'src/engine/metadata-modules/permissions/types/user-workspace-permissions'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; @@ -22,6 +25,8 @@ export class PermissionsService { constructor( private readonly userRoleService: UserRoleService, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + @InjectRepository(RoleEntity, 'core') + private readonly roleRepository: Repository, ) {} public async getUserWorkspacePermissions({ @@ -51,17 +56,17 @@ export class PermissionsService { hasPermissionOnSettingFeature = true; } - const settingPermissions = roleOfUserWorkspace.settingPermissions ?? []; + const permissionFlags = roleOfUserWorkspace.permissionFlags ?? []; const defaultSettingsPermissions = this.getDefaultUserWorkspacePermissions().settingsPermissions; - const settingsPermissions = Object.keys(SettingPermissionType).reduce( + const settingsPermissions = Object.keys(PermissionFlagType).reduce( (acc, feature) => ({ ...acc, [feature]: hasPermissionOnSettingFeature || - settingPermissions.some( - (settingPermission) => settingPermission.setting === feature, + permissionFlags.some( + (permissionFlag) => permissionFlag.flag === feature, ), }), defaultSettingsPermissions, @@ -102,14 +107,15 @@ export class PermissionsService { [PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]: false, }, settingsPermissions: { - [SettingPermissionType.API_KEYS_AND_WEBHOOKS]: false, - [SettingPermissionType.WORKSPACE]: false, - [SettingPermissionType.WORKSPACE_MEMBERS]: false, - [SettingPermissionType.ROLES]: false, - [SettingPermissionType.DATA_MODEL]: false, - [SettingPermissionType.ADMIN_PANEL]: false, - [SettingPermissionType.SECURITY]: false, - [SettingPermissionType.WORKFLOWS]: false, + [PermissionFlagType.API_KEYS_AND_WEBHOOKS]: false, + [PermissionFlagType.WORKSPACE]: false, + [PermissionFlagType.WORKSPACE_MEMBERS]: false, + [PermissionFlagType.ROLES]: false, + [PermissionFlagType.DATA_MODEL]: false, + [PermissionFlagType.ADMIN_PANEL]: false, + [PermissionFlagType.SECURITY]: false, + [PermissionFlagType.WORKFLOWS]: false, + [PermissionFlagType.SEND_EMAIL_TOOL]: false, }, objectPermissions: {}, }) as const satisfies UserWorkspacePermissions; @@ -122,7 +128,7 @@ export class PermissionsService { }: { userWorkspaceId?: string; workspaceId: string; - setting: SettingPermissionType; + setting: PermissionFlagType; isExecutedByApiKey: boolean; }): Promise { if (isExecutedByApiKey) { @@ -154,10 +160,39 @@ export class PermissionsService { return true; } - const settingPermissions = roleOfUserWorkspace.settingPermissions ?? []; + const permissionFlags = roleOfUserWorkspace.permissionFlags ?? []; - return settingPermissions.some( - (settingPermission) => settingPermission.setting === setting, + return permissionFlags.some( + (permissionFlag) => permissionFlag.flag === setting, ); } + + public async hasToolPermission( + roleId: string, + workspaceId: string, + flag: PermissionFlagType, + ): Promise { + try { + const role = await this.roleRepository.findOne({ + where: { id: roleId, workspaceId }, + relations: ['permissionFlags'], + }); + + if (!role) { + return false; + } + + if (role.canAccessAllTools === true) { + return true; + } + + const permissionFlags = role.permissionFlags ?? []; + + return permissionFlags.some( + (permissionFlag) => permissionFlag.flag === flag, + ); + } catch (error) { + return false; + } + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/types/user-workspace-permissions.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/types/user-workspace-permissions.ts index 473873c3f2..731ac70840 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/types/user-workspace-permissions.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/types/user-workspace-permissions.ts @@ -1,10 +1,10 @@ import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; import { ObjectRecordsPermissions } from 'twenty-shared/types'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; export type UserWorkspacePermissions = { - settingsPermissions: Record; + settingsPermissions: Record; objectRecordsPermissions: Record; objectPermissions: ObjectRecordsPermissions; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/dtos/create-role-input.dto.ts b/packages/twenty-server/src/engine/metadata-modules/role/dtos/create-role-input.dto.ts index eaf68cb50f..0426e91415 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/dtos/create-role-input.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/dtos/create-role-input.dto.ts @@ -28,6 +28,11 @@ export class CreateRoleInput { @Field({ nullable: true }) canUpdateAllSettings?: boolean; + @IsBoolean() + @IsOptional() + @Field({ nullable: true }) + canAccessAllTools?: boolean; + @IsBoolean() @IsOptional() @Field({ nullable: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts b/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts index e499a9be84..3b527a3b07 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts @@ -4,8 +4,8 @@ import { Relation } from 'typeorm'; import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto'; +import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; -import { SettingPermissionDTO } from 'src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto'; @ObjectType('Role') export class RoleDTO { @@ -33,6 +33,9 @@ export class RoleDTO { @Field({ nullable: false }) canUpdateAllSettings: boolean; + @Field({ nullable: false }) + canAccessAllTools: boolean; + @Field({ nullable: false }) canReadAllObjectRecords: boolean; @@ -45,8 +48,8 @@ export class RoleDTO { @Field({ nullable: false }) canDestroyAllObjectRecords: boolean; - @Field(() => [SettingPermissionDTO], { nullable: true }) - settingPermissions?: SettingPermissionDTO[]; + @Field(() => [PermissionFlagDTO], { nullable: true }) + permissionFlags?: PermissionFlagDTO[]; @Field(() => [ObjectPermissionDTO], { nullable: true }) objectPermissions?: ObjectPermissionDTO[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/dtos/update-role-input.dto.ts b/packages/twenty-server/src/engine/metadata-modules/role/dtos/update-role-input.dto.ts index c355f77c43..b10d9490d5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/dtos/update-role-input.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/dtos/update-role-input.dto.ts @@ -32,6 +32,11 @@ export class UpdateRolePayload { @Field({ nullable: true }) canUpdateAllSettings?: boolean; + @IsBoolean() + @IsOptional() + @Field({ nullable: true }) + canAccessAllTools?: boolean; + @IsBoolean() @IsOptional() @Field({ nullable: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts index 18951be977..016fe1877b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts @@ -11,8 +11,8 @@ import { import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; +import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; -import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity'; @Entity('role') @Unique('IDX_ROLE_LABEL_WORKSPACE_ID_UNIQUE', ['label', 'workspaceId']) @@ -26,6 +26,9 @@ export class RoleEntity { @Column({ nullable: false, default: false }) canUpdateAllSettings: boolean; + @Column({ nullable: false, default: false }) + canAccessAllTools: boolean; + @Column({ nullable: false, default: false }) canReadAllObjectRecords: boolean; @@ -69,10 +72,10 @@ export class RoleEntity { objectPermissions: Relation; @OneToMany( - () => SettingPermissionEntity, - (settingPermission: SettingPermissionEntity) => settingPermission.role, + () => PermissionFlagEntity, + (permissionFlag: PermissionFlagEntity) => permissionFlag.role, ) - settingPermissions: Relation; + permissionFlags: Relation; @OneToMany( () => FieldPermissionEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts index d4e7e8c3fe..f5b03a26c3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts @@ -7,12 +7,12 @@ import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AgentRoleModule } from 'src/engine/metadata-modules/agent-role/agent-role.module'; import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module'; +import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleResolver } from 'src/engine/metadata-modules/role/role.resolver'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; -import { SettingPermissionModule } from 'src/engine/metadata-modules/setting-permission/setting-permission.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; @@ -25,7 +25,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor PermissionsModule, UserWorkspaceModule, ObjectPermissionModule, - SettingPermissionModule, + PermissionFlagModule, WorkspacePermissionsCacheModule, FileModule, ], diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts index 23f9fb39f6..b443755e2a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts @@ -28,7 +28,10 @@ import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object- import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input'; import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service'; import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto'; +import { UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input'; +import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -39,9 +42,6 @@ import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-ro import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; import { UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; -import { SettingPermissionDTO } from 'src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto'; -import { UpsertSettingPermissionsInput } from 'src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input'; -import { SettingPermissionService } from 'src/engine/metadata-modules/setting-permission/setting-permission.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -49,7 +49,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @UsePipes(ResolverValidationPipe) @UseGuards( WorkspaceAuthGuard, - SettingsPermissionsGuard(SettingPermissionType.ROLES), + SettingsPermissionsGuard(PermissionFlagType.ROLES), ) @UseFilters( PermissionsGraphqlApiExceptionFilter, @@ -61,7 +61,7 @@ export class RoleResolver { private readonly roleService: RoleService, private readonly userWorkspaceService: UserWorkspaceService, private readonly objectPermissionService: ObjectPermissionService, - private readonly settingPermissionService: SettingPermissionService, + private readonly settingPermissionService: PermissionFlagService, private readonly agentRoleService: AgentRoleService, private readonly fieldPermissionService: FieldPermissionService, ) {} @@ -171,15 +171,15 @@ export class RoleResolver { }); } - @Mutation(() => [SettingPermissionDTO]) - async upsertSettingPermissions( + @Mutation(() => [PermissionFlagDTO]) + async upsertPermissionFlags( @AuthWorkspace() workspace: Workspace, - @Args('upsertSettingPermissionsInput') - upsertSettingPermissionsInput: UpsertSettingPermissionsInput, - ): Promise { - return this.settingPermissionService.upsertSettingPermissions({ + @Args('upsertPermissionFlagsInput') + upsertPermissionFlagsInput: UpsertPermissionFlagsInput, + ): Promise { + return this.settingPermissionService.upsertPermissionFlags({ workspaceId: workspace.id, - input: upsertSettingPermissionsInput, + input: upsertPermissionFlagsInput, }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts index 47e609e802..b53fd07305 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts @@ -40,7 +40,7 @@ export class RoleService { where: { workspaceId, }, - relations: ['roleTargets', 'settingPermissions', 'objectPermissions'], + relations: ['roleTargets', 'permissionFlags', 'objectPermissions'], }); } @@ -53,7 +53,7 @@ export class RoleService { id, workspaceId, }, - relations: ['roleTargets', 'settingPermissions'], + relations: ['roleTargets', 'permissionFlags'], }); } @@ -72,6 +72,7 @@ export class RoleService { description: input.description, icon: input.icon, canUpdateAllSettings: input.canUpdateAllSettings, + canAccessAllTools: input.canAccessAllTools, canReadAllObjectRecords: input.canReadAllObjectRecords, canUpdateAllObjectRecords: input.canUpdateAllObjectRecords, canSoftDeleteAllObjectRecords: input.canSoftDeleteAllObjectRecords, @@ -143,6 +144,7 @@ export class RoleService { description: 'Admin role', icon: 'IconUserCog', canUpdateAllSettings: true, + canAccessAllTools: true, canReadAllObjectRecords: true, canUpdateAllObjectRecords: true, canSoftDeleteAllObjectRecords: true, @@ -209,6 +211,7 @@ export class RoleService { description: 'Member role', icon: 'IconUser', canUpdateAllSettings: false, + canAccessAllTools: false, canReadAllObjectRecords: true, canUpdateAllObjectRecords: true, canSoftDeleteAllObjectRecords: true, @@ -229,6 +232,7 @@ export class RoleService { description: 'Guest role', icon: 'IconUser', canUpdateAllSettings: false, + canAccessAllTools: false, canReadAllObjectRecords: true, canUpdateAllObjectRecords: false, canSoftDeleteAllObjectRecords: false, @@ -250,6 +254,7 @@ export class RoleService { const keysToValidate = [ 'label', 'canUpdateAllSettings', + 'canAccessAllTools', 'canReadAllObjectRecords', 'canUpdateAllObjectRecords', 'canSoftDeleteAllObjectRecords', diff --git a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts index 7b5f232411..cc8a300eec 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts @@ -6,6 +6,7 @@ export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => { id: role.id, label: role.label, canUpdateAllSettings: role.canUpdateAllSettings, + canAccessAllTools: role.canAccessAllTools, description: role.description, icon: role.icon, isEditable: role.isEditable, diff --git a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts index b73b773ecf..a6d64cf12d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto.ts @@ -1,6 +1,6 @@ import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { UserWorkspacePermissions } from 'src/engine/metadata-modules/permissions/types/user-workspace-permissions'; import { UserWorkspacePermissionsDto } from 'src/engine/metadata-modules/role/dtos/user-workspace-permissions.dto'; @@ -20,7 +20,7 @@ export const fromUserWorkspacePermissionsToUserWorkspacePermissionsDto = ({ ); const settingsPermissions = ( - Object.keys(rawSettingsPermissions) as SettingPermissionType[] + Object.keys(rawSettingsPermissions) as PermissionFlagType[] ).filter((feature) => rawSettingsPermissions[feature] === true); const objectRecordsPermissions = ( diff --git a/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto.ts b/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto.ts deleted file mode 100644 index ed2cf28946..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Field, ObjectType } from '@nestjs/graphql'; - -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; - -@ObjectType('SettingPermission') -export class SettingPermissionDTO { - @Field({ nullable: false }) - id: string; - - @Field({ nullable: false }) - roleId: string; - - @Field({ nullable: false }) - setting: SettingPermissionType; -} diff --git a/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input.ts b/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input.ts deleted file mode 100644 index c7f679e4fb..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Field, InputType } from '@nestjs/graphql'; - -import { IsArray, IsEnum, IsNotEmpty, IsUUID } from 'class-validator'; - -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; - -@InputType() -export class UpsertSettingPermissionsInput { - @IsUUID() - @IsNotEmpty() - @Field() - roleId: string; - - @IsArray() - @IsEnum(SettingPermissionType, { each: true }) - @Field(() => [SettingPermissionType]) - settingPermissionKeys: SettingPermissionType[]; -} diff --git a/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.module.ts b/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.module.ts deleted file mode 100644 index 3fa3192b6d..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/setting-permission/setting-permission.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity'; -import { SettingPermissionService } from 'src/engine/metadata-modules/setting-permission/setting-permission.service'; -import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([SettingPermissionEntity, RoleEntity], 'core'), - WorkspacePermissionsCacheModule, - ], - - providers: [SettingPermissionService], - exports: [SettingPermissionService], -}) -export class SettingPermissionModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts index 015a9deb7a..7d9459d01c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts @@ -105,7 +105,7 @@ export class UserRoleService { }, relations: { role: { - settingPermissions: true, + permissionFlags: true, }, }, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts index 583ac4861c..b5fdc2ba73 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts @@ -11,7 +11,7 @@ import { In, Repository } from 'typeorm'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; @@ -184,7 +184,7 @@ export class WorkspacePermissionsCacheService { }, relations: [ 'objectPermissions', - 'settingPermissions', + 'permissionFlags', ...(isFieldPermissionsEnabled ? ['fieldPermissions'] : []), ], }); @@ -317,9 +317,9 @@ export class WorkspacePermissionsCacheService { private hasWorkflowsPermissions(role: RoleEntity): boolean { const hasWorkflowsPermissionFromRole = role.canUpdateAllSettings; const hasWorkflowsPermissionsFromSettingPermissions = isDefined( - role.settingPermissions.find( - (settingPermission) => - settingPermission.setting === SettingPermissionType.WORKFLOWS, + role.permissionFlags.find( + (permissionFlag) => + permissionFlag.flag === PermissionFlagType.WORKFLOWS, ), ); diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts index 36a6719318..9e2860c6e2 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts @@ -13,6 +13,7 @@ import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/worksp import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { entitySchemaFactories } from 'src/engine/twenty-orm/factories'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; +import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @@ -46,6 +47,7 @@ import { PgPoolSharedModule } from './pg-shared-pool/pg-shared-pool.module'; TwentyORMManager, TwentyORMGlobalManager, PgPoolSharedModule, + ScopedWorkspaceContextFactory, ], }) export class TwentyORMModule {} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts index f0a0aa9718..d4aa2a0ae3 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; @@ -54,7 +53,6 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess [Workspace, DataSourceEntity, ObjectMetadataEntity], 'core', ), - BillingModule, EmailAliasManagerModule, FeatureFlagModule, MessageParticipantManagerModule, diff --git a/packages/twenty-server/src/modules/messaging/messaging.module.ts b/packages/twenty-server/src/modules/messaging/messaging.module.ts index 24376567ca..698f1ef999 100644 --- a/packages/twenty-server/src/modules/messaging/messaging.module.ts +++ b/packages/twenty-server/src/modules/messaging/messaging.module.ts @@ -15,6 +15,6 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess MessagingMonitoringModule, ], providers: [], - exports: [], + exports: [MessagingImportManagerModule], }) export class MessagingModule {} diff --git a/packages/twenty-server/src/modules/messaging/monitoring/messaging-monitoring.module.ts b/packages/twenty-server/src/modules/messaging/monitoring/messaging-monitoring.module.ts index 6d2cb7e6a9..cde67c7ff0 100644 --- a/packages/twenty-server/src/modules/messaging/monitoring/messaging-monitoring.module.ts +++ b/packages/twenty-server/src/modules/messaging/monitoring/messaging-monitoring.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; -import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; @@ -14,7 +13,6 @@ import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/ser imports: [ AuditModule, MessagingCommonModule, - BillingModule, TypeOrmModule.forFeature([Workspace], 'core'), TypeOrmModule.forFeature([DataSourceEntity], 'core'), ], diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/factories/workflow-action.factory.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/factories/workflow-action.factory.ts index a009e47280..dc282d6a85 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/factories/workflow-action.factory.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/factories/workflow-action.factory.ts @@ -10,26 +10,24 @@ import { AiAgentWorkflowAction } from 'src/modules/workflow/workflow-executor/wo import { CodeWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code.workflow-action'; import { FilterWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter.workflow-action'; import { FormWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form.workflow-action'; -import { SendEmailWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action'; import { CreateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action'; import { DeleteRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action'; import { FindRecordsWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action'; import { UpdateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action'; +import { ToolExecutorWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action'; import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; -import { WorkflowActionAdapter } from 'src/modules/workflow/workflow-executor/workflow-actions/workflow-action-adapter'; @Injectable() export class WorkflowActionFactory { constructor( private readonly codeWorkflowAction: CodeWorkflowAction, - private readonly sendEmailWorkflowAction: SendEmailWorkflowAction, private readonly createRecordWorkflowAction: CreateRecordWorkflowAction, private readonly updateRecordWorkflowAction: UpdateRecordWorkflowAction, private readonly deleteRecordWorkflowAction: DeleteRecordWorkflowAction, private readonly findRecordsWorkflowAction: FindRecordsWorkflowAction, private readonly formWorkflowAction: FormWorkflowAction, private readonly filterWorkflowAction: FilterWorkflowAction, - private readonly workflowActionAdapter: WorkflowActionAdapter, + private readonly toolExecutorWorkflowAction: ToolExecutorWorkflowAction, private readonly aiAgentWorkflowAction: AiAgentWorkflowAction, ) {} @@ -38,7 +36,7 @@ export class WorkflowActionFactory { case WorkflowActionType.CODE: return this.codeWorkflowAction; case WorkflowActionType.SEND_EMAIL: - return this.sendEmailWorkflowAction; + return this.toolExecutorWorkflowAction; case WorkflowActionType.CREATE_RECORD: return this.createRecordWorkflowAction; case WorkflowActionType.UPDATE_RECORD: @@ -52,7 +50,7 @@ export class WorkflowActionFactory { case WorkflowActionType.FILTER: return this.filterWorkflowAction; case WorkflowActionType.HTTP_REQUEST: - return this.workflowActionAdapter; + return this.toolExecutorWorkflowAction; case WorkflowActionType.AI_AGENT: return this.aiAgentWorkflowAction; default: diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email-action.module.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email-action.module.ts deleted file mode 100644 index 79ed349264..0000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email-action.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; -import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module'; -import { SendEmailWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action'; - -@Module({ - imports: [MessagingImportManagerModule], - providers: [ScopedWorkspaceContextFactory, SendEmailWorkflowAction], - exports: [SendEmailWorkflowAction], -}) -export class SendEmailActionModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts deleted file mode 100644 index c6e264f87e..0000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import DOMPurify from 'dompurify'; -import { JSDOM } from 'jsdom'; -import { isDefined, isValidUuid } from 'twenty-shared/utils'; -import { z } from 'zod'; - -import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface'; - -import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service'; -import { - WorkflowStepExecutorException, - WorkflowStepExecutorExceptionCode, -} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception'; -import { WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input'; -import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type'; -import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util'; -import { - SendEmailActionException, - SendEmailActionExceptionCode, -} from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/exceptions/send-email-action.exception'; -import { isWorkflowSendEmailAction } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/guards/is-workflow-send-email-action.guard'; -import { WorkflowSendEmailActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-input.type'; - -export type WorkflowSendEmailStepOutputSchema = { - success: boolean; -}; - -@Injectable() -export class SendEmailWorkflowAction implements WorkflowAction { - private readonly logger = new Logger(SendEmailWorkflowAction.name); - constructor( - private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly sendMessageService: MessagingSendMessageService, - ) {} - - private async getConnectedAccount(connectedAccountId: string) { - if (!isValidUuid(connectedAccountId)) { - throw new SendEmailActionException( - `Connected Account ID is not a valid UUID`, - SendEmailActionExceptionCode.INVALID_CONNECTED_ACCOUNT_ID, - ); - } - - const { workspaceId } = this.scopedWorkspaceContextFactory.create(); - - if (!workspaceId) { - throw new WorkflowStepExecutorException( - 'Scoped workspace not found', - WorkflowStepExecutorExceptionCode.SCOPED_WORKSPACE_NOT_FOUND, - ); - } - - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); - const connectedAccount = await connectedAccountRepository.findOneBy({ - id: connectedAccountId, - }); - - if (!isDefined(connectedAccount)) { - throw new SendEmailActionException( - `Connected Account '${connectedAccountId}' not found`, - SendEmailActionExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, - ); - } - - return connectedAccount; - } - - async execute({ - currentStepId, - steps, - context, - }: WorkflowActionInput): Promise { - const step = steps.find((step) => step.id === currentStepId); - - if (!step) { - throw new WorkflowStepExecutorException( - 'Step not found', - WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND, - ); - } - - if (!isWorkflowSendEmailAction(step)) { - throw new WorkflowStepExecutorException( - 'Step is not a send email action', - WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE, - ); - } - - const connectedAccount = await this.getConnectedAccount( - step.settings.input.connectedAccountId, - ); - - const workflowActionInput = resolveInput( - step.settings.input, - context, - ) as WorkflowSendEmailActionInput; - - const { email, body, subject } = workflowActionInput; - - const emailSchema = z.string().trim().email('Invalid email'); - - const result = emailSchema.safeParse(email); - - if (!result.success) { - throw new SendEmailActionException( - `Email '${email}' invalid`, - SendEmailActionExceptionCode.INVALID_EMAIL, - ); - } - - const window = new JSDOM('').window; - const purify = DOMPurify(window); - const safeBody = purify.sanitize(body || ''); - const safeSubject = purify.sanitize(subject || ''); - - await this.sendMessageService.sendMessage( - { - to: email, - subject: safeSubject, - body: safeBody, - }, - connectedAccount, - ); - - this.logger.log(`Email sent successfully`); - - return { - result: { success: true } satisfies WorkflowSendEmailStepOutputSchema, - }; - } -} diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/workflow-action-adapter.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action.ts similarity index 84% rename from packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/workflow-action-adapter.ts rename to packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action.ts index b1251bd7f2..6542323ef7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/workflow-action-adapter.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface'; -import { TOOLS } from 'src/engine/core-modules/tool/constants/tools.const'; import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum'; +import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service'; import { ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; import { WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input'; import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type'; @@ -11,7 +11,9 @@ import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/varia import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; @Injectable() -export class WorkflowActionAdapter implements WorkflowAction { +export class ToolExecutorWorkflowAction implements WorkflowAction { + constructor(private readonly toolRegistry: ToolRegistryService) {} + async execute({ currentStepId, steps, @@ -31,7 +33,7 @@ export class WorkflowActionAdapter implements WorkflowAction { ); } - const tool = TOOLS.get(toolType); + const tool = this.toolRegistry.getTool(toolType); if (!tool) { throw new Error( @@ -54,6 +56,7 @@ export class WorkflowActionAdapter implements WorkflowAction { ): ToolType | null { const mapping: Partial> = { [WorkflowActionType.HTTP_REQUEST]: ToolType.HTTP_REQUEST, + [WorkflowActionType.SEND_EMAIL]: ToolType.SEND_EMAIL, }; return mapping[actionType] || null; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts index e3e12e24f3..1cba6cae86 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { AiModule } from 'src/engine/core-modules/ai/ai.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; @@ -9,9 +10,8 @@ import { AiAgentActionModule } from 'src/modules/workflow/workflow-executor/work import { CodeActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code-action.module'; import { FilterActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter-action.module'; import { FormActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form-action.module'; -import { SendEmailActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email-action.module'; import { RecordCRUDActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module'; -import { WorkflowActionAdapter } from 'src/modules/workflow/workflow-executor/workflow-actions/workflow-action-adapter'; +import { ToolExecutorWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action'; import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service'; import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module'; @@ -19,7 +19,6 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow imports: [ WorkflowCommonModule, CodeActionModule, - SendEmailActionModule, RecordCRUDActionModule, FormActionModule, WorkflowRunModule, @@ -27,12 +26,13 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow FilterActionModule, AiAgentActionModule, FeatureFlagModule, + AiModule, ], providers: [ WorkflowExecutorWorkspaceService, ScopedWorkspaceContextFactory, WorkflowActionFactory, - WorkflowActionAdapter, + ToolExecutorWorkflowAction, ], exports: [WorkflowExecutorWorkspaceService], }) diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts index 885006244d..738c93709f 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsException, PermissionsExceptionCode, @@ -50,7 +50,7 @@ export class WorkspaceMemberPreQueryHookService { await this.permissionsService.userHasWorkspaceSettingPermission({ userWorkspaceId, workspaceId, - setting: SettingPermissionType.WORKSPACE_MEMBERS, + setting: PermissionFlagType.WORKSPACE_MEMBERS, isExecutedByApiKey: isDefined(apiKey), }) ) { diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.integration-spec.ts index 7de00cb7fb..33ad84e1e6 100644 --- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.integration-spec.ts @@ -7,7 +7,7 @@ import { createOneObjectMetadataQueryFactory } from 'test/integration/metadata/s import { deleteOneObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata-query-factory.util'; import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant'; @@ -70,13 +70,13 @@ describe('Granular settings permissions', () => { // Assign specific setting permissions to the custom role const upsertSettingPermissionsQuery = { query: ` - mutation UpsertSettingPermissions { - upsertSettingPermissions(upsertSettingPermissionsInput: { + mutation UpsertPermissionFlags { + upsertPermissionFlags(upsertPermissionFlagsInput: { roleId: "${customRoleId}" - settingPermissionKeys: [${SettingPermissionType.DATA_MODEL}, ${SettingPermissionType.WORKSPACE}, ${SettingPermissionType.WORKFLOWS}] + permissionFlagKeys: [${PermissionFlagType.DATA_MODEL}, ${PermissionFlagType.WORKSPACE}, ${PermissionFlagType.WORKFLOWS}] }) { id - setting + flag roleId } } @@ -357,8 +357,8 @@ describe('Granular settings permissions', () => { id label canUpdateAllSettings - settingPermissions { - setting + permissionFlags { + flag } } } @@ -376,13 +376,13 @@ describe('Granular settings permissions', () => { expect(customRole).toBeDefined(); expect(customRole.canUpdateAllSettings).toBe(false); - expect(customRole.settingPermissions).toHaveLength(3); - expect( - customRole.settingPermissions.map((p: any) => p.setting), - ).toContain(SettingPermissionType.DATA_MODEL); - expect( - customRole.settingPermissions.map((p: any) => p.setting), - ).toContain(SettingPermissionType.WORKSPACE); + expect(customRole.permissionFlags).toHaveLength(3); + expect(customRole.permissionFlags.map((p: any) => p.flag)).toContain( + PermissionFlagType.DATA_MODEL, + ); + expect(customRole.permissionFlags.map((p: any) => p.flag)).toContain( + PermissionFlagType.WORKSPACE, + ); }); }); @@ -391,13 +391,13 @@ describe('Granular settings permissions', () => { // Add SECURITY permission to the custom role const upsertSecurityPermissionQuery = { query: ` - mutation UpsertSettingPermissions { - upsertSettingPermissions(upsertSettingPermissionsInput: { + mutation UpsertPermissionFlags { + upsertPermissionFlags(upsertPermissionFlagsInput: { roleId: "${customRoleId}" - settingPermissionKeys: [${SettingPermissionType.DATA_MODEL}, ${SettingPermissionType.WORKSPACE}, ${SettingPermissionType.SECURITY}] + permissionFlagKeys: [${PermissionFlagType.DATA_MODEL}, ${PermissionFlagType.WORKSPACE}, ${PermissionFlagType.SECURITY}] }) { id - setting + flag roleId } } @@ -411,7 +411,7 @@ describe('Granular settings permissions', () => { expect(response.status).toBe(200); expect(response.body.errors).toBeUndefined(); - expect(response.body.data.upsertSettingPermissions).toHaveLength(3); + expect(response.body.data.upsertPermissionFlags).toHaveLength(3); // Verify the user now has access to security operations // Note: This would require a specific security operation to test @@ -421,8 +421,8 @@ describe('Granular settings permissions', () => { query GetRole { getRoles { id - settingPermissions { - setting + permissionFlags { + flag } } } @@ -438,23 +438,23 @@ describe('Granular settings permissions', () => { (role: any) => role.id === customRoleId, ); - expect(updatedRole.settingPermissions).toHaveLength(3); - expect( - updatedRole.settingPermissions.map((p: any) => p.setting), - ).toContain(SettingPermissionType.SECURITY); + expect(updatedRole.permissionFlags).toHaveLength(3); + expect(updatedRole.permissionFlags.map((p: any) => p.flag)).toContain( + PermissionFlagType.SECURITY, + ); }); it('should allow removing setting permissions from existing role', async () => { // Remove SECURITY permission, keep only DATA_MODEL and WORKSPACE const upsertReducedPermissionsQuery = { query: ` - mutation UpsertSettingPermissions { - upsertSettingPermissions(upsertSettingPermissionsInput: { + mutation UpsertPermissionFlags { + upsertPermissionFlags(upsertPermissionFlagsInput: { roleId: "${customRoleId}" - settingPermissionKeys: [${SettingPermissionType.DATA_MODEL}, ${SettingPermissionType.WORKSPACE}] + permissionFlagKeys: [${PermissionFlagType.DATA_MODEL}, ${PermissionFlagType.WORKSPACE}] }) { id - setting + flag roleId } } @@ -468,7 +468,7 @@ describe('Granular settings permissions', () => { expect(response.status).toBe(200); expect(response.body.errors).toBeUndefined(); - expect(response.body.data.upsertSettingPermissions).toHaveLength(2); + expect(response.body.data.upsertPermissionFlags).toHaveLength(2); // Verify SECURITY permission was removed const getRoleQuery = { @@ -476,8 +476,8 @@ describe('Granular settings permissions', () => { query GetRole { getRoles { id - settingPermissions { - setting + permissionFlags { + flag } } } @@ -493,10 +493,10 @@ describe('Granular settings permissions', () => { (role: any) => role.id === customRoleId, ); - expect(updatedRole.settingPermissions).toHaveLength(2); - expect( - updatedRole.settingPermissions.map((p: any) => p.setting), - ).not.toContain(SettingPermissionType.SECURITY); + expect(updatedRole.permissionFlags).toHaveLength(2); + expect(updatedRole.permissionFlags.map((p: any) => p.flag)).not.toContain( + PermissionFlagType.SECURITY, + ); }); }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts index 9693d2a546..3114d93a2c 100644 --- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts @@ -5,7 +5,7 @@ import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object import { fieldTextMock } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant'; @@ -561,17 +561,17 @@ describe('roles permissions', () => { }); }); - describe('upsertSettingPermissions', () => { + describe('upsertPermissionFlags', () => { const upsertSettingPermissionsMutation = ({ roleId, }: { roleId: string; }) => ` - mutation UpsertSettingPermissions { - upsertSettingPermissions(upsertSettingPermissionsInput: {roleId: "${roleId}", settingPermissionKeys: [${SettingPermissionType.DATA_MODEL}]}) { + mutation UpsertPermissionFlags { + upsertPermissionFlags(upsertPermissionFlagsInput: {roleId: "${roleId}", permissionFlagKeys: [${PermissionFlagType.DATA_MODEL}]}) { id roleId - setting + flag } } `; @@ -625,11 +625,11 @@ describe('roles permissions', () => { .expect((res) => { expect(res.body.data).toBeDefined(); expect(res.body.errors).toBeUndefined(); - expect(res.body.data.upsertSettingPermissions).toEqual( + expect(res.body.data.upsertPermissionFlags).toEqual( expect.arrayContaining([ expect.objectContaining({ roleId: createdEditableRoleId, - setting: SettingPermissionType.DATA_MODEL, + flag: PermissionFlagType.DATA_MODEL, }), ]), ); diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts index 48ea47921a..7da2f66f9d 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts @@ -5,14 +5,19 @@ import { Repository } from 'typeorm'; import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service'; import { ToolService } from 'src/engine/core-modules/ai/services/tool.service'; +import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service'; +import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool'; import { AgentToolService } from 'src/engine/metadata-modules/agent/agent-tool.service'; import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity'; import { AgentService } from 'src/engine/metadata-modules/agent/agent.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service'; import { getMockObjectMetadataEntity } from 'src/utils/__test__/get-object-metadata-entity.mock'; export interface AgentToolTestContext { @@ -79,6 +84,34 @@ export const createAgentToolTestModule = provide: ToolAdapterService, useClass: ToolAdapterService, }, + { + provide: ToolRegistryService, + useClass: ToolRegistryService, + }, + { + provide: SendEmailTool, + useValue: { + description: 'mock', + parameters: {}, + execute: jest.fn(), + }, + }, + { + provide: ScopedWorkspaceContextFactory, + useValue: { + create: jest.fn(() => ({ workspaceId: 'test-workspace-id' })), + }, + }, + { + provide: MessagingSendMessageService, + useValue: { sendMessage: jest.fn() }, + }, + { + provide: PermissionsService, + useValue: { + hasToolPermission: jest.fn(), + }, + }, ], }).compile();