Decouple Send Email node from workflows (#13322)

- 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 <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Abdul Rahman
2025-07-24 16:01:33 +05:30
committed by GitHub
parent eb404478c3
commit e93adde4b8
98 changed files with 1076 additions and 705 deletions

View File

@ -548,6 +548,7 @@ export type CreateRemoteServerInput = {
};
export type CreateRoleInput = {
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canSoftDeleteAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
@ -1202,7 +1203,7 @@ export type Mutation = {
uploadWorkspaceLogo: SignedFileDto;
upsertFieldPermissions: Array<FieldPermission>;
upsertObjectPermissions: Array<ObjectPermission>;
upsertSettingPermissions: Array<SettingPermission>;
upsertPermissionFlags: Array<PermissionFlag>;
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<Scalars['ConnectionCursor']>;
};
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<Array<ObjectPermission>>;
settingPermissions?: Maybe<Array<SettingPermission>>;
permissionFlags?: Maybe<Array<PermissionFlag>>;
workspaceMembers: Array<WorkspaceMember>;
};
@ -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<Scalars['Boolean']>;
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canSoftDeleteAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
@ -2752,9 +2756,9 @@ export type UpsertObjectPermissionsInput = {
roleId: Scalars['String'];
};
export type UpsertSettingPermissionsInput = {
export type UpsertPermissionFlagsInput = {
permissionFlagKeys: Array<PermissionFlagType>;
roleId: Scalars['String'];
settingPermissionKeys: Array<SettingPermissionType>;
};
export type User = {
@ -2832,7 +2836,7 @@ export type UserWorkspace = {
objectPermissions?: Maybe<Array<ObjectPermission>>;
/** @deprecated Use objectPermissions instead */
objectRecordsPermissions?: Maybe<Array<PermissionsOnAllObjectRecords>>;
settingsPermissions?: Maybe<Array<SettingPermissionType>>;
settingsPermissions?: Maybe<Array<PermissionFlagType>>;
twoFactorAuthenticationMethodSummary?: Maybe<Array<TwoFactorAuthenticationMethodDto>>;
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<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<PermissionFlagType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<PermissionFlagType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<typeof useUpsertObjectPermissionsMutation>;
export type UpsertObjectPermissionsMutationResult = Apollo.MutationResult<UpsertObjectPermissionsMutation>;
export type UpsertObjectPermissionsMutationOptions = Apollo.BaseMutationOptions<UpsertObjectPermissionsMutation, UpsertObjectPermissionsMutationVariables>;
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<UpsertSettingPermissionsMutation, UpsertSettingPermissionsMutationVariables>;
${PermissionFlagFragmentFragmentDoc}`;
export type UpsertPermissionFlagsMutationFn = Apollo.MutationFunction<UpsertPermissionFlagsMutation, UpsertPermissionFlagsMutationVariables>;
/**
* __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<UpsertSettingPermissionsMutation, UpsertSettingPermissionsMutationVariables>) {
export function useUpsertPermissionFlagsMutation(baseOptions?: Apollo.MutationHookOptions<UpsertPermissionFlagsMutation, UpsertPermissionFlagsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpsertSettingPermissionsMutation, UpsertSettingPermissionsMutationVariables>(UpsertSettingPermissionsDocument, options);
return Apollo.useMutation<UpsertPermissionFlagsMutation, UpsertPermissionFlagsMutationVariables>(UpsertPermissionFlagsDocument, options);
}
export type UpsertSettingPermissionsMutationHookResult = ReturnType<typeof useUpsertSettingPermissionsMutation>;
export type UpsertSettingPermissionsMutationResult = Apollo.MutationResult<UpsertSettingPermissionsMutation>;
export type UpsertSettingPermissionsMutationOptions = Apollo.BaseMutationOptions<UpsertSettingPermissionsMutation, UpsertSettingPermissionsMutationVariables>;
export type UpsertPermissionFlagsMutationHookResult = ReturnType<typeof useUpsertPermissionFlagsMutation>;
export type UpsertPermissionFlagsMutationResult = Apollo.MutationResult<UpsertPermissionFlagsMutation>;
export type UpsertPermissionFlagsMutationOptions = Apollo.BaseMutationOptions<UpsertPermissionFlagsMutation, UpsertPermissionFlagsMutationVariables>;
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}`;
/**

View File

@ -512,6 +512,7 @@ export type CreateOneFieldMetadataInput = {
};
export type CreateRoleInput = {
canAccessAllTools?: InputMaybe<Scalars['Boolean']>;
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canSoftDeleteAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
@ -1153,7 +1154,7 @@ export type Mutation = {
uploadWorkspaceLogo: SignedFileDto;
upsertFieldPermissions: Array<FieldPermission>;
upsertObjectPermissions: Array<ObjectPermission>;
upsertSettingPermissions: Array<SettingPermission>;
upsertPermissionFlags: Array<PermissionFlag>;
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<Scalars['ConnectionCursor']>;
};
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<Array<ObjectPermission>>;
settingPermissions?: Maybe<Array<SettingPermission>>;
permissionFlags?: Maybe<Array<PermissionFlag>>;
workspaceMembers: Array<WorkspaceMember>;
};
@ -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<Scalars['Boolean']>;
canDestroyAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canReadAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
canSoftDeleteAllObjectRecords?: InputMaybe<Scalars['Boolean']>;
@ -2590,9 +2594,9 @@ export type UpsertObjectPermissionsInput = {
roleId: Scalars['String'];
};
export type UpsertSettingPermissionsInput = {
export type UpsertPermissionFlagsInput = {
permissionFlagKeys: Array<PermissionFlagType>;
roleId: Scalars['String'];
settingPermissionKeys: Array<SettingPermissionType>;
};
export type User = {
@ -2660,7 +2664,7 @@ export type UserWorkspace = {
objectPermissions?: Maybe<Array<ObjectPermission>>;
/** @deprecated Use objectPermissions instead */
objectRecordsPermissions?: Maybe<Array<PermissionsOnAllObjectRecords>>;
settingsPermissions?: Maybe<Array<SettingPermissionType>>;
settingsPermissions?: Maybe<Array<PermissionFlagType>>;
twoFactorAuthenticationMethodSummary?: Maybe<Array<TwoFactorAuthenticationMethodDto>>;
updatedAt: Scalars['DateTime'];
user: User;

View File

@ -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 = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingPermissionType.WORKSPACE}
settingsPermission={PermissionFlagType.WORKSPACE}
/>
}
>
@ -416,7 +416,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingPermissionType.WORKSPACE_MEMBERS}
settingsPermission={PermissionFlagType.WORKSPACE_MEMBERS}
/>
}
>
@ -428,7 +428,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingPermissionType.DATA_MODEL}
settingsPermission={PermissionFlagType.DATA_MODEL}
/>
}
>
@ -458,7 +458,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingPermissionType.ROLES}
settingsPermission={PermissionFlagType.ROLES}
/>
}
>
@ -480,7 +480,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingPermissionType.API_KEYS_AND_WEBHOOKS}
settingsPermission={PermissionFlagType.API_KEYS_AND_WEBHOOKS}
/>
}
>
@ -555,7 +555,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingPermissionType.SECURITY}
settingsPermission={PermissionFlagType.SECURITY}
/>
}
>
@ -587,7 +587,7 @@ export const SettingsRoutes = ({
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={SettingPermissionType.WORKSPACE}
settingsPermission={PermissionFlagType.WORKSPACE}
/>
}
>

View File

@ -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 = () => {

View File

@ -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 (

View File

@ -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 = () => {

View File

@ -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 (

View File

@ -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,

View File

@ -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;
};

View File

@ -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(), {

View File

@ -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`,

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const PERMISSION_FLAG_FRAGMENT = gql`
fragment PermissionFlagFragment on PermissionFlag {
id
flag
roleId
}
`;

View File

@ -7,6 +7,7 @@ export const ROLE_FRAGMENT = gql`
description
icon
canUpdateAllSettings
canAccessAllTools
isEditable
canReadAllObjectRecords
canUpdateAllObjectRecords

View File

@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const SETTING_PERMISSION_FRAGMENT = gql`
fragment SettingPermissionFragment on SettingPermission {
id
setting
roleId
}
`;

View File

@ -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
}
}
`;

View File

@ -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
}
}
`;

View File

@ -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

View File

@ -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);
};

View File

@ -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,
);

View File

@ -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}
/>
<SettingsRolePermissionsToolSection
roleId={roleId}
isEditable={isEditable}
/>
</StyledRolePermissionsContainer>
);
};

View File

@ -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,

View File

@ -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,
}))
: [],

View File

@ -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,
),
});
}

View File

@ -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 (
<Section>
<H2Title
title={t`Action Permissions`}
description={t`Permissions for performing automated actions.`}
/>
<StyledCard rounded>
<SettingsOptionCardContentToggle
Icon={IconTool}
title={t`All Actions Access`}
description={t`Grants permission to perform all available actions without restriction.`}
checked={settingsDraftRole.canAccessAllTools}
disabled={!isEditable}
onChange={() => {
setSettingsDraftRole({
...settingsDraftRole,
canAccessAllTools: !settingsDraftRole.canAccessAllTools,
});
}}
/>
</StyledCard>
<AnimatedExpandableContainer
isExpanded={!settingsDraftRole.canAccessAllTools}
dimension="height"
animationDurations={{
opacity: 0.2,
size: 0.4,
}}
mode="scroll-height"
containAnimation={false}
>
<StyledTable>
<SettingsRolePermissionsSettingsTableHeader
roleId={roleId}
settingsPermissionsConfig={toolPermissionsConfig}
isEditable={isEditable}
/>
<StyledTableRows>
{toolPermissionsConfig.map((permission) => (
<SettingsRolePermissionsSettingsTableRow
key={permission.key}
roleId={roleId}
permission={permission}
isEditable={isEditable}
/>
))}
</StyledTableRows>
</StyledTable>
</AnimatedExpandableContainer>
</Section>
);
};

View File

@ -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;
};

View File

@ -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<keyof Role> = [
'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:

View File

@ -42,6 +42,7 @@ export const SettingsRoleCreateEffect = ({
description: '',
icon: 'IconUser',
canUpdateAllSettings: true,
canAccessAllTools: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,

View File

@ -13,9 +13,10 @@ export const settingsDraftRoleFamilyState = createFamilyState<Role, string>({
canSoftDeleteAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canUpdateAllSettings: false,
canAccessAllTools: false,
isEditable: false,
workspaceMembers: [],
settingPermissions: [],
permissionFlags: [],
objectPermissions: [],
},
});

View File

@ -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]],
},

View File

@ -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,

View File

@ -0,0 +1,46 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RenameSettingPermissionToPermissionFlag1753149175945
implements MigrationInterface
{
name = 'RenameSettingPermissionToPermissionFlag1753149175945';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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`,
);
}
}

View File

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddCanAccessAllToolsColumnToRole1753318977613
implements MigrationInterface
{
name = 'AddCanAccessAllToolsColumnToRole1753318977613';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(
`ALTER TABLE "core"."role" DROP COLUMN "canAccessAllTools"`,
);
}
}

View File

@ -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;

View File

@ -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

View File

@ -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 {}

View File

@ -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<ToolSet>(
(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<ToolSet> {
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),
};
}
}

View File

@ -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(

View File

@ -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,
});

View File

@ -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,

View File

@ -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) {}

View File

@ -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) {}

View File

@ -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<ToolType, Tool> = new Map([
[ToolType.HTTP_REQUEST, new HttpTool()],
]);

View File

@ -1,3 +1,4 @@
export enum ToolType {
HTTP_REQUEST = 'HTTP_REQUEST',
SEND_EMAIL = 'SEND_EMAIL',
}

View File

@ -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<ToolType, () => Tool>;
constructor(private readonly sendEmailTool: SendEmailTool) {
this.toolFactories = new Map<ToolType, () => 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());
}
}

View File

@ -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',
}

View File

@ -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,
});

View File

@ -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<ConnectedAccountWorkspaceEntity>(
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<string> {
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
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<ToolOutput> {
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',
};
}
}
}

View File

@ -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<typeof SendEmailInputZodSchema>;

View File

@ -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<ToolOutput>;
flag?: PermissionFlagType;
};

View File

@ -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<TwoFactorAuthenticationMethod[]>;
@Field(() => [SettingPermissionType], { nullable: true })
settingsPermissions?: SettingPermissionType[];
@Field(() => [PermissionFlagType], { nullable: true })
settingsPermissions?: PermissionFlagType[];
@Field(() => [PermissionsOnAllObjectRecords], {
nullable: true,

View File

@ -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(

View File

@ -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,

View File

@ -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(

View File

@ -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,

View File

@ -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(

View File

@ -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<Workspace> {
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<Workspace> {
await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId,
workspaceId,
setting: SettingPermissionType.WORKSPACE,
setting: PermissionFlagType.WORKSPACE,
isExecutedByApiKey: isDefined(apiKey),
});

View File

@ -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);

View File

@ -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<CanActivate> => {
@Injectable()
class SettingsPermissionsMixin implements CanActivate {

View File

@ -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,

View File

@ -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.

View File

@ -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<UpdateFieldInput>,
) {}
@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,

View File

@ -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 },

View File

@ -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,

View File

@ -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;
}

View File

@ -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[];
}

View File

@ -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<RoleEntity>;
@Column({ nullable: false, type: 'varchar' })
setting: SettingPermissionType;
flag: PermissionFlagType;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;

View File

@ -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 {}

View File

@ -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<SettingPermissionEntity>,
@InjectRepository(PermissionFlagEntity, 'core')
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
@InjectRepository(RoleEntity, 'core')
private readonly roleRepository: Repository<RoleEntity>,
@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<SettingPermissionEntity[]> {
input: UpsertPermissionFlagsInput;
}): Promise<PermissionFlagEntity[]> {
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();

View File

@ -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',
}

View File

@ -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<RoleEntity>,
) {}
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<boolean> {
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<boolean> {
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;
}
}
}

View File

@ -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<SettingPermissionType, boolean>;
settingsPermissions: Record<PermissionFlagType, boolean>;
objectRecordsPermissions: Record<PermissionsOnAllObjectRecords, boolean>;
objectPermissions: ObjectRecordsPermissions;
};

View File

@ -28,6 +28,11 @@ export class CreateRoleInput {
@Field({ nullable: true })
canUpdateAllSettings?: boolean;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })
canAccessAllTools?: boolean;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })

View File

@ -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[];

View File

@ -32,6 +32,11 @@ export class UpdateRolePayload {
@Field({ nullable: true })
canUpdateAllSettings?: boolean;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })
canAccessAllTools?: boolean;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })

View File

@ -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<ObjectPermissionEntity[]>;
@OneToMany(
() => SettingPermissionEntity,
(settingPermission: SettingPermissionEntity) => settingPermission.role,
() => PermissionFlagEntity,
(permissionFlag: PermissionFlagEntity) => permissionFlag.role,
)
settingPermissions: Relation<SettingPermissionEntity[]>;
permissionFlags: Relation<PermissionFlagEntity[]>;
@OneToMany(
() => FieldPermissionEntity,

View File

@ -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,
],

View File

@ -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<SettingPermissionDTO[]> {
return this.settingPermissionService.upsertSettingPermissions({
@Args('upsertPermissionFlagsInput')
upsertPermissionFlagsInput: UpsertPermissionFlagsInput,
): Promise<PermissionFlagDTO[]> {
return this.settingPermissionService.upsertPermissionFlags({
workspaceId: workspace.id,
input: upsertSettingPermissionsInput,
input: upsertPermissionFlagsInput,
});
}

View File

@ -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',

View File

@ -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,

View File

@ -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 = (

View File

@ -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;
}

View File

@ -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[];
}

View File

@ -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 {}

View File

@ -105,7 +105,7 @@ export class UserRoleService {
},
relations: {
role: {
settingPermissions: true,
permissionFlags: true,
},
},
});

View File

@ -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,
),
);

View File

@ -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 {}

View File

@ -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,

View File

@ -15,6 +15,6 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
MessagingMonitoringModule,
],
providers: [],
exports: [],
exports: [MessagingImportManagerModule],
})
export class MessagingModule {}

View File

@ -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'),
],

View File

@ -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:

View File

@ -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 {}

View File

@ -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<ConnectedAccountWorkspaceEntity>(
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<WorkflowActionOutput> {
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,
};
}
}

View File

@ -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<Record<WorkflowActionType, ToolType>> = {
[WorkflowActionType.HTTP_REQUEST]: ToolType.HTTP_REQUEST,
[WorkflowActionType.SEND_EMAIL]: ToolType.SEND_EMAIL,
};
return mapping[actionType] || null;

View File

@ -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],
})

View File

@ -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),
})
) {

View File

@ -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,
);
});
});
});

View File

@ -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,
}),
]),
);

View File

@ -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();