diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomain.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomain.tsx index bab6cb6893..fc53227c55 100644 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomain.tsx +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomain.tsx @@ -4,7 +4,6 @@ import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { Controller, useFormContext } from 'react-hook-form'; import { SettingsCustomDomainRecords } from '~/pages/settings/workspace/SettingsCustomDomainRecords'; -import { SettingsCustomDomainRecordsStatus } from '~/pages/settings/workspace/SettingsCustomDomainRecordsStatus'; import { customDomainRecordsState } from '~/pages/settings/workspace/states/customDomainRecordsState'; import { useRecoilValue } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; @@ -79,33 +78,31 @@ export const SettingsCustomDomain = () => { /> )} /> - - - - + {currentWorkspace?.customDomain && ( + + + + + )} {currentWorkspace?.customDomain && ( - - {customDomainRecords && - customDomainRecords.records.some( - (record) => record.status !== 'success', - ) && ( - - )} + {customDomainRecords && ( + + )} )} diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx index 04a938c8c0..aa4f9208eb 100644 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx @@ -6,8 +6,17 @@ import { TableRow } from '@/ui/layout/table/components/TableRow'; import styled from '@emotion/styled'; import { Button } from 'twenty-ui/input'; import { useDebouncedCallback } from 'use-debounce'; -import { CustomDomainValidRecords } from '~/generated/graphql'; +import { + CustomDomainRecord, + CustomDomainValidRecords, +} from '~/generated/graphql'; import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; +import { capitalize } from 'twenty-shared/utils'; +import { useRecoilValue } from 'recoil'; +import { customDomainRecordsState } from '~/pages/settings/workspace/states/customDomainRecordsState'; +import { ThemeColor } from 'twenty-ui/theme'; +import { Status } from 'twenty-ui/display'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; const StyledTable = styled(Table)` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; @@ -42,6 +51,9 @@ export const SettingsCustomDomainRecords = ({ }: { records: CustomDomainValidRecords['records']; }) => { + const { customDomainRecords } = useRecoilValue(customDomainRecordsState); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const { copyToClipboard } = useCopyToClipboard(); const copyToClipboardDebounced = useDebouncedCallback( @@ -49,43 +61,82 @@ export const SettingsCustomDomainRecords = ({ 200, ); + const rowsDefinitions = [ + { name: 'Domain Setup', validationType: 'redirection' as const }, + { name: 'Secure Connection', validationType: 'ssl' as const }, + ]; + + const defaultValues: { status: string; color: ThemeColor } = + currentWorkspace?.customDomain === customDomainRecords?.customDomain + ? { + status: 'success', + color: 'green', + } + : { + status: 'loading', + color: 'gray', + }; + + const rows = rowsDefinitions.map< + { name: string; status: string; color: ThemeColor } & CustomDomainRecord + >((row) => { + const record = records.find( + ({ validationType }) => validationType === row.validationType, + ); + + if (!record) { + throw new Error(`Record ${row.name} not found`); + } + + return { + name: row.name, + color: + record && record.status === 'error' + ? 'red' + : record && record.status === 'pending' + ? 'yellow' + : defaultValues.color, + ...record, + }; + }); + return ( - + Name Type Value + - {records - .filter((record) => record.status !== 'success') - .map((record) => ( - - - copyToClipboardDebounced(record.key)} - type="button" - /> - - - - copyToClipboardDebounced(record.type.toUpperCase()) - } - type="button" - /> - - - copyToClipboardDebounced(record.value)} - type="button" - /> - - - ))} + {rows.map((row) => ( + + + copyToClipboardDebounced(row.key)} + type="button" + /> + + + copyToClipboardDebounced(row.type.toUpperCase())} + type="button" + /> + + + copyToClipboardDebounced(row.value)} + type="button" + /> + + + + + + ))} ); diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecordsStatus.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecordsStatus.tsx deleted file mode 100644 index d566e518f1..0000000000 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecordsStatus.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Table } from '@/ui/layout/table/components/Table'; -import { TableRow } from '@/ui/layout/table/components/TableRow'; -import { TableCell } from '@/ui/layout/table/components/TableCell'; -import styled from '@emotion/styled'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { customDomainRecordsState } from '~/pages/settings/workspace/states/customDomainRecordsState'; -import { useRecoilValue } from 'recoil'; -import { capitalize } from 'twenty-shared/utils'; -import { Status } from 'twenty-ui/display'; -import { ThemeColor } from 'twenty-ui/theme'; - -const StyledTable = styled(Table)` - background-color: ${({ theme }) => theme.background.transparent.lighter}; - border: 1px solid ${({ theme }) => theme.border.color.light}; - padding: 0 ${({ theme }) => theme.spacing(2)}; - border-radius: ${({ theme }) => theme.border.radius.sm}; -`; - -const StyledTableRow = styled(TableRow)` - display: flex; - border-bottom: 1px solid ${({ theme }) => theme.border.color.medium}; - border-radius: 0; - align-items: center; - justify-content: space-between; - padding: 0; - font-size: ${({ theme }) => theme.font.size.sm}; - &:last-child { - border-bottom: none; - } -`; - -const StyledTableCell = styled(TableCell)` - padding: 0; -`; - -const records = [ - { name: 'CNAME', validationType: 'redirection' as const }, - { name: 'TXT Validation', validationType: 'ownership' as const }, - { name: 'SSL Certificate Generation', validationType: 'ssl' as const }, -]; - -export const SettingsCustomDomainRecordsStatus = () => { - const currentWorkspace = useRecoilValue(currentWorkspaceState); - - const { customDomainRecords } = useRecoilValue(customDomainRecordsState); - - const defaultValues: { status: string; color: ThemeColor } = - currentWorkspace?.customDomain === customDomainRecords?.customDomain - ? { - status: 'success', - color: 'green', - } - : { - status: 'loading', - color: 'gray', - }; - - const rows = records.map<{ name: string; status: string; color: ThemeColor }>( - (record) => { - const foundRecord = customDomainRecords?.records.find( - ({ validationType }) => validationType === record.validationType, - ); - return { - name: record.name, - status: foundRecord ? foundRecord.status : defaultValues.status, - color: - foundRecord && foundRecord.status === 'error' - ? 'red' - : foundRecord && foundRecord.status === 'pending' - ? 'yellow' - : defaultValues.color, - }; - }, - ); - - return ( - - {rows.map((row) => ( - - {row.name} - - - - - ))} - - ); -}; diff --git a/packages/twenty-front/src/pages/settings/workspace/hooks/useCheckCustomDomainValidRecords.ts b/packages/twenty-front/src/pages/settings/workspace/hooks/useCheckCustomDomainValidRecords.ts index 611e30e90a..4c5fcf9db1 100644 --- a/packages/twenty-front/src/pages/settings/workspace/hooks/useCheckCustomDomainValidRecords.ts +++ b/packages/twenty-front/src/pages/settings/workspace/hooks/useCheckCustomDomainValidRecords.ts @@ -2,10 +2,12 @@ import { customDomainRecordsState } from '~/pages/settings/workspace/states/cust import { useCheckCustomDomainValidRecordsMutation } from '~/generated-metadata/graphql'; import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; export const useCheckCustomDomainValidRecords = () => { const [checkCustomDomainValidRecords] = useCheckCustomDomainValidRecordsMutation(); + const { enqueueErrorSnackBar } = useSnackBar(); const setCustomDomainRecords = useSetRecoilState(customDomainRecordsState); @@ -24,6 +26,13 @@ export const useCheckCustomDomainValidRecords = () => { : {}), })); }, + onError: (error) => { + enqueueErrorSnackBar({ apolloError: error }); + setCustomDomainRecords((currentState) => ({ + ...currentState, + isLoading: false, + })); + }, }); }; diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 6479da50d2..31f90aca05 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -41,7 +41,7 @@ "cache-manager-redis-yet": "^4.1.2", "class-validator": "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch", "class-validator-jsonschema": "^5.0.2", - "cloudflare": "^4.0.0", + "cloudflare": "^4.5.0", "connect-redis": "^7.1.1", "express-session": "^1.18.1", "graphql-middleware": "^6.1.35", diff --git a/packages/twenty-server/src/database/commands/cron-register-all.command.ts b/packages/twenty-server/src/database/commands/cron-register-all.command.ts index a47f4f8369..4f13794235 100644 --- a/packages/twenty-server/src/database/commands/cron-register-all.command.ts +++ b/packages/twenty-server/src/database/commands/cron-register-all.command.ts @@ -10,6 +10,7 @@ import { MessagingMessageListFetchCronCommand } from 'src/modules/messaging/mess import { MessagingMessagesImportCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-messages-import.cron.command'; import { MessagingOngoingStaleCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-ongoing-stale.cron.command'; import { CronTriggerCronCommand } from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/commands/cron-trigger.cron.command'; +import { CheckCustomDomainValidRecordsCronCommand } from 'src/engine/core-modules/domain-manager/crons/commands/check-custom-domain-valid-records.cron.command'; @Command({ name: 'cron:register:all', @@ -27,6 +28,7 @@ export class CronRegisterAllCommand extends CommandRunner { private readonly calendarOngoingStaleCronCommand: CalendarOngoingStaleCronCommand, private readonly cronTriggerCronCommand: CronTriggerCronCommand, private readonly cleanupOrphanedFilesCronCommand: CleanupOrphanedFilesCronCommand, + private readonly checkCustomDomainValidRecordsCronCommand: CheckCustomDomainValidRecordsCronCommand, ) { super(); } @@ -64,6 +66,10 @@ export class CronRegisterAllCommand extends CommandRunner { name: 'CleanupOrphanedFiles', command: this.cleanupOrphanedFilesCronCommand, }, + { + name: 'CheckCustomDomainValidRecords', + command: this.checkCustomDomainValidRecordsCronCommand, + }, ]; let successCount = 0; diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index bd6f5e2e50..16e43f875c 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -19,6 +19,7 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m import { CalendarEventImportManagerModule } from 'src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module'; import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module'; import { AutomatedTriggerModule } from 'src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.module'; +import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { DataSeedWorkspaceCommand } from './data-seed-dev-workspace.command'; @@ -33,6 +34,7 @@ import { DataSeedWorkspaceCommand } from './data-seed-dev-workspace.command'; CalendarEventImportManagerModule, AutomatedTriggerModule, FileModule, + DomainManagerModule, // Data seeding dependencies TypeORMModule, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts index 7acac330fa..4f279828ca 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts @@ -1,11 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class GraphqlQueryRunnerException extends CustomException { - declare code: GraphqlQueryRunnerExceptionCode; - constructor(message: string, code: GraphqlQueryRunnerExceptionCode) { - super(message, code); - } -} +export class GraphqlQueryRunnerException extends CustomException {} export enum GraphqlQueryRunnerExceptionCode { INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT', diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts index 9a7ca365f0..f6ae911f37 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts @@ -1,20 +1,19 @@ -import { CustomException } from 'src/utils/custom-exception'; +import { + appendCommonExceptionCode, + CustomException, +} from 'src/utils/custom-exception'; -export class WorkspaceQueryRunnerException extends CustomException { - code: WorkspaceQueryRunnerExceptionCode; - constructor(message: string, code: WorkspaceQueryRunnerExceptionCode) { - super(message, code); - this.code = code; - } -} +export class WorkspaceQueryRunnerException extends CustomException< + keyof typeof WorkspaceQueryRunnerExceptionCode +> {} -export enum WorkspaceQueryRunnerExceptionCode { - INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT', - DATA_NOT_FOUND = 'DATA_NOT_FOUND', - QUERY_TIMEOUT = 'QUERY_TIMEOUT', - QUERY_VIOLATES_UNIQUE_CONSTRAINT = 'QUERY_VIOLATES_UNIQUE_CONSTRAINT', - QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT = 'QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT', - TOO_MANY_ROWS_AFFECTED = 'TOO_MANY_ROWS_AFFECTED', - NO_ROWS_AFFECTED = 'NO_ROWS_AFFECTED', - INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', -} +export const WorkspaceQueryRunnerExceptionCode = appendCommonExceptionCode({ + INVALID_QUERY_INPUT: 'INVALID_QUERY_INPUT', + DATA_NOT_FOUND: 'DATA_NOT_FOUND', + QUERY_TIMEOUT: 'QUERY_TIMEOUT', + QUERY_VIOLATES_UNIQUE_CONSTRAINT: 'QUERY_VIOLATES_UNIQUE_CONSTRAINT', + QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT: + 'QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT', + TOO_MANY_ROWS_AFFECTED: 'TOO_MANY_ROWS_AFFECTED', + NO_ROWS_AFFECTED: 'NO_ROWS_AFFECTED', +} as const); diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key.exception.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key.exception.ts index a017db1b9e..0d15a6a634 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key.exception.ts @@ -1,15 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class ApiKeyException extends CustomException { - declare code: ApiKeyExceptionCode; - constructor( - message: string, - code: ApiKeyExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class ApiKeyException extends CustomException {} export enum ApiKeyExceptionCode { API_KEY_NOT_FOUND = 'API_KEY_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.exception.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.exception.ts index 1cbde171e6..de009387dc 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.exception.ts @@ -1,15 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class ApprovedAccessDomainException extends CustomException { - declare code: ApprovedAccessDomainExceptionCode; - constructor( - message: string, - code: ApprovedAccessDomainExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class ApprovedAccessDomainException extends CustomException {} export enum ApprovedAccessDomainExceptionCode { APPROVED_ACCESS_DOMAIN_NOT_FOUND = 'APPROVED_ACCESS_DOMAIN_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/core-modules/audit/audit.exception.ts b/packages/twenty-server/src/engine/core-modules/audit/audit.exception.ts index fdb4ea65bd..b87410d8ab 100644 --- a/packages/twenty-server/src/engine/core-modules/audit/audit.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/audit/audit.exception.ts @@ -1,11 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class AuditException extends CustomException { - declare code: AuditExceptionCode; - constructor(message: string, code: AuditExceptionCode) { - super(message, code); - } -} +export class AuditException extends CustomException {} export enum AuditExceptionCode { INVALID_TYPE = 'INVALID_TYPE', diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts index 919d9eba3d..8b61edac75 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts @@ -1,36 +1,33 @@ -import { CustomException } from 'src/utils/custom-exception'; +import { + appendCommonExceptionCode, + CustomException, +} from 'src/utils/custom-exception'; -export class AuthException extends CustomException { - declare code: AuthExceptionCode; - constructor( - message: string, - code: AuthExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class AuthException extends CustomException< + keyof typeof AuthExceptionCode +> {} -export enum AuthExceptionCode { - USER_NOT_FOUND = 'USER_NOT_FOUND', - USER_WORKSPACE_NOT_FOUND = 'USER_WORKSPACE_NOT_FOUND', - EMAIL_NOT_VERIFIED = 'EMAIL_NOT_VERIFIED', - CLIENT_NOT_FOUND = 'CLIENT_NOT_FOUND', - WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', - INVALID_INPUT = 'INVALID_INPUT', - FORBIDDEN_EXCEPTION = 'FORBIDDEN_EXCEPTION', - INSUFFICIENT_SCOPES = 'INSUFFICIENT_SCOPES', - UNAUTHENTICATED = 'UNAUTHENTICATED', - INVALID_DATA = 'INVALID_DATA', - INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', - OAUTH_ACCESS_DENIED = 'OAUTH_ACCESS_DENIED', - SSO_AUTH_FAILED = 'SSO_AUTH_FAILED', - USE_SSO_AUTH = 'USE_SSO_AUTH', - SIGNUP_DISABLED = 'SIGNUP_DISABLED', - GOOGLE_API_AUTH_DISABLED = 'GOOGLE_API_AUTH_DISABLED', - MICROSOFT_API_AUTH_DISABLED = 'MICROSOFT_API_AUTH_DISABLED', - MISSING_ENVIRONMENT_VARIABLE = 'MISSING_ENVIRONMENT_VARIABLE', - INVALID_JWT_TOKEN_TYPE = 'INVALID_JWT_TOKEN_TYPE', - TWO_FACTOR_AUTHENTICATION_PROVISION_REQUIRED = 'TWO_FACTOR_AUTHENTICATION_PROVISION_REQUIRED', - TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED = 'TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED', -} +export const AuthExceptionCode = appendCommonExceptionCode({ + USER_NOT_FOUND: 'USER_NOT_FOUND', + USER_WORKSPACE_NOT_FOUND: 'USER_WORKSPACE_NOT_FOUND', + EMAIL_NOT_VERIFIED: 'EMAIL_NOT_VERIFIED', + CLIENT_NOT_FOUND: 'CLIENT_NOT_FOUND', + WORKSPACE_NOT_FOUND: 'WORKSPACE_NOT_FOUND', + INVALID_INPUT: 'INVALID_INPUT', + FORBIDDEN_EXCEPTION: 'FORBIDDEN_EXCEPTION', + INSUFFICIENT_SCOPES: 'INSUFFICIENT_SCOPES', + UNAUTHENTICATED: 'UNAUTHENTICATED', + INVALID_DATA: 'INVALID_DATA', + OAUTH_ACCESS_DENIED: 'OAUTH_ACCESS_DENIED', + SSO_AUTH_FAILED: 'SSO_AUTH_FAILED', + USE_SSO_AUTH: 'USE_SSO_AUTH', + SIGNUP_DISABLED: 'SIGNUP_DISABLED', + GOOGLE_API_AUTH_DISABLED: 'GOOGLE_API_AUTH_DISABLED', + MICROSOFT_API_AUTH_DISABLED: 'MICROSOFT_API_AUTH_DISABLED', + MISSING_ENVIRONMENT_VARIABLE: 'MISSING_ENVIRONMENT_VARIABLE', + INVALID_JWT_TOKEN_TYPE: 'INVALID_JWT_TOKEN_TYPE', + TWO_FACTOR_AUTHENTICATION_PROVISION_REQUIRED: + 'TWO_FACTOR_AUTHENTICATION_PROVISION_REQUIRED', + TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED: + 'TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED', +} as const); diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts index e2d6691339..22f6720f2e 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts @@ -2,11 +2,7 @@ import { CustomException } from 'src/utils/custom-exception'; -export class BillingException extends CustomException { - constructor(message: string, code: BillingExceptionCode) { - super(message, code); - } -} +export class BillingException extends CustomException {} export enum BillingExceptionCode { BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/core-modules/captcha/captcha.exception.ts b/packages/twenty-server/src/engine/core-modules/captcha/captcha.exception.ts index 09d0f5ffd6..602a14d5b6 100644 --- a/packages/twenty-server/src/engine/core-modules/captcha/captcha.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/captcha/captcha.exception.ts @@ -1,15 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class CaptchaException extends CustomException { - declare code: CaptchaExceptionCode; - constructor( - message: string, - code: CaptchaExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class CaptchaException extends CustomException {} export enum CaptchaExceptionCode { INVALID_CAPTCHA = 'INVALID_CAPTCHA', diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/controllers/cloudflare.controller.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/controllers/cloudflare.controller.ts index cd84c13949..4c8a46e51d 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/controllers/cloudflare.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/controllers/cloudflare.controller.ts @@ -42,58 +42,79 @@ export class CloudflareController { @Post(['cloudflare/custom-hostname-webhooks', 'webhooks/cloudflare']) @UseGuards(CloudflareSecretMatchGuard, PublicEndpointGuard) async customHostnameWebhooks(@Req() req: Request, @Res() res: Response) { - if (!req.body?.data?.data?.hostname) { + try { + // Cloudflare documentation is inaccurate - some webhooks lack the hostname field. + // Fallback to extracting hostname from validation_records. + const hostname = + req.body?.data?.data?.hostname ?? + req.body?.data?.data?.ssl?.validation_records?.[0]?.txt_name?.replace( + /^_acme-challenge\./, + '', + ); + + if (!hostname) { + handleException({ + exception: new DomainManagerException( + 'Hostname missing', + DomainManagerExceptionCode.INVALID_INPUT_DATA, + { userFriendlyMessage: 'Hostname missing' }, + ), + exceptionHandlerService: this.exceptionHandlerService, + }); + + return res.status(200).send(); + } + + const workspace = await this.workspaceRepository.findOneBy({ + customDomain: hostname, + }); + + if (!workspace) return; + + const analytics = this.auditService.createContext({ + workspaceId: workspace.id, + }); + + const customDomainDetails = + await this.customDomainService.getCustomDomainDetails(hostname); + + const workspaceUpdated: Partial = { + customDomain: workspace.customDomain, + }; + + if (!customDomainDetails) { + workspaceUpdated.customDomain = null; + } + + workspaceUpdated.isCustomDomainEnabled = customDomainDetails + ? this.domainManagerService.isCustomDomainWorking(customDomainDetails) + : false; + + if ( + workspaceUpdated.isCustomDomainEnabled !== + workspace.isCustomDomainEnabled || + workspaceUpdated.customDomain !== workspace.customDomain + ) { + await this.workspaceRepository.save({ + ...workspace, + ...workspaceUpdated, + }); + + await analytics.insertWorkspaceEvent(CUSTOM_DOMAIN_ACTIVATED_EVENT, {}); + } + + return res.status(200).send(); + } catch (err) { handleException({ exception: new DomainManagerException( - 'Hostname missing', - DomainManagerExceptionCode.INVALID_INPUT_DATA, + err.message ?? 'Unknown error occurred', + DomainManagerExceptionCode.INTERNAL_SERVER_ERROR, + { userFriendlyMessage: 'Unknown error occurred' }, ), exceptionHandlerService: this.exceptionHandlerService, }); return res.status(200).send(); } - - const workspace = await this.workspaceRepository.findOneBy({ - customDomain: req.body.data.data.hostname, - }); - - if (!workspace) return; - - const analytics = this.auditService.createContext({ - workspaceId: workspace.id, - }); - - const customDomainDetails = - await this.customDomainService.getCustomDomainDetails( - req.body.data.data.hostname, - ); - - const workspaceUpdated: Partial = { - customDomain: workspace.customDomain, - }; - - if (!customDomainDetails && workspace) { - workspaceUpdated.customDomain = null; - } - - workspaceUpdated.isCustomDomainEnabled = customDomainDetails - ? this.domainManagerService.isCustomDomainWorking(customDomainDetails) - : false; - - if ( - workspaceUpdated.isCustomDomainEnabled !== - workspace.isCustomDomainEnabled || - workspaceUpdated.customDomain !== workspace.customDomain - ) { - await this.workspaceRepository.save({ - ...workspace, - ...workspaceUpdated, - }); - - await analytics.insertWorkspaceEvent(CUSTOM_DOMAIN_ACTIVATED_EVENT, {}); - } - - return res.status(200).send(); } } diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/crons/commands/check-custom-domain-valid-records.cron.command.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/crons/commands/check-custom-domain-valid-records.cron.command.ts new file mode 100644 index 0000000000..18a49323b2 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/crons/commands/check-custom-domain-valid-records.cron.command.ts @@ -0,0 +1,32 @@ +import { Command, CommandRunner } from 'nest-commander'; + +import { + CHECK_CUSTOM_DOMAIN_VALID_RECORDS_CRON_PATTERN, + CheckCustomDomainValidRecordsCronJob, +} from 'src/engine/core-modules/domain-manager/crons/jobs/check-custom-domain-valid-records.cron.job'; +import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; + +@Command({ + name: 'cron:domain-manager:check-custom-domain-valid-records', + description: 'Starts a cron job to check custom domain valid records hourly', +}) +export class CheckCustomDomainValidRecordsCronCommand extends CommandRunner { + constructor( + @InjectMessageQueue(MessageQueue.cronQueue) + private readonly messageQueueService: MessageQueueService, + ) { + super(); + } + + async run(): Promise { + await this.messageQueueService.addCron({ + jobName: CheckCustomDomainValidRecordsCronJob.name, + data: undefined, + options: { + repeat: { pattern: CHECK_CUSTOM_DOMAIN_VALID_RECORDS_CRON_PATTERN }, + }, + }); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/crons/jobs/check-custom-domain-valid-records.cron.job.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/crons/jobs/check-custom-domain-valid-records.cron.job.ts new file mode 100644 index 0000000000..1e45201a54 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/crons/jobs/check-custom-domain-valid-records.cron.job.ts @@ -0,0 +1,50 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { IsNull, Not, Repository, Raw } from 'typeorm'; +import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; + +import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; +import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service'; +import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator'; + +export const CHECK_CUSTOM_DOMAIN_VALID_RECORDS_CRON_PATTERN = '0 * * * *'; + +@Processor(MessageQueue.cronQueue) +export class CheckCustomDomainValidRecordsCronJob { + constructor( + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + private readonly customDomainService: CustomDomainService, + ) {} + + @Process(CheckCustomDomainValidRecordsCronJob.name) + @SentryCronMonitor( + CheckCustomDomainValidRecordsCronJob.name, + CHECK_CUSTOM_DOMAIN_VALID_RECORDS_CRON_PATTERN, + ) + async handle(): Promise { + const workspaces = await this.workspaceRepository.find({ + where: { + activationStatus: WorkspaceActivationStatus.ACTIVE, + customDomain: Not(IsNull()), + createdAt: Raw( + (alias) => `EXTRACT(HOUR FROM ${alias}) = EXTRACT(HOUR FROM NOW())`, + ), + }, + select: ['id', 'customDomain', 'isCustomDomainEnabled'], + }); + + for (const workspace of workspaces) { + try { + await this.customDomainService.checkCustomDomainValidRecords(workspace); + } catch (error) { + throw new Error( + `[${CheckCustomDomainValidRecordsCronJob.name}] Cannot check custom domain for workspaces: ${error.message}`, + ); + } + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.exception.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.exception.ts index 3637cea58a..879667ea69 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.exception.ts @@ -1,14 +1,15 @@ -import { CustomException } from 'src/utils/custom-exception'; +import { + appendCommonExceptionCode, + CustomException, +} from 'src/utils/custom-exception'; -export class DomainManagerException extends CustomException { - constructor(message: string, code: DomainManagerExceptionCode) { - super(message, code); - } -} +export class DomainManagerException extends CustomException< + keyof typeof DomainManagerExceptionCode, + true +> {} -export enum DomainManagerExceptionCode { - CLOUDFLARE_CLIENT_NOT_INITIALIZED = 'CLOUDFLARE_CLIENT_NOT_INITIALIZED', - HOSTNAME_ALREADY_REGISTERED = 'HOSTNAME_ALREADY_REGISTERED', - SUBDOMAIN_REQUIRED = 'SUBDOMAIN_REQUIRED', - INVALID_INPUT_DATA = 'INVALID_INPUT_DATA', -} +export const DomainManagerExceptionCode = appendCommonExceptionCode({ + CLOUDFLARE_CLIENT_NOT_INITIALIZED: 'CLOUDFLARE_CLIENT_NOT_INITIALIZED', + HOSTNAME_ALREADY_REGISTERED: 'HOSTNAME_ALREADY_REGISTERED', + INVALID_INPUT_DATA: 'INVALID_INPUT_DATA', +} as const); diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts index 282522fd22..b38c000bf3 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts @@ -3,14 +3,27 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { CloudflareController } from 'src/engine/core-modules/domain-manager/controllers/cloudflare.controller'; +import { CheckCustomDomainValidRecordsCronCommand } from 'src/engine/core-modules/domain-manager/crons/commands/check-custom-domain-valid-records.cron.command'; +import { CheckCustomDomainValidRecordsCronJob } from 'src/engine/core-modules/domain-manager/crons/jobs/check-custom-domain-valid-records.cron.job'; import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DomainManagerResolver } from 'src/engine/core-modules/domain-manager/domain-manager.resolver'; @Module({ imports: [AuditModule, TypeOrmModule.forFeature([Workspace], 'core')], - providers: [DomainManagerService, CustomDomainService], - exports: [DomainManagerService, CustomDomainService], + providers: [ + DomainManagerResolver, + DomainManagerService, + CustomDomainService, + CheckCustomDomainValidRecordsCronJob, + CheckCustomDomainValidRecordsCronCommand, + ], + exports: [ + DomainManagerService, + CustomDomainService, + CheckCustomDomainValidRecordsCronCommand, + ], controllers: [CloudflareController], }) export class DomainManagerModule {} diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.resolver.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.resolver.ts new file mode 100644 index 0000000000..22e0f28c02 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.resolver.ts @@ -0,0 +1,23 @@ +import { Mutation, Resolver } from '@nestjs/graphql'; +import { UseGuards, UsePipes } from '@nestjs/common'; + +import { CustomDomainValidRecords } from 'src/engine/core-modules/domain-manager/dtos/custom-domain-valid-records'; +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service'; +import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; + +@UsePipes(ResolverValidationPipe) +@Resolver() +export class DomainManagerResolver { + constructor(private readonly customDomainService: CustomDomainService) {} + + @Mutation(() => CustomDomainValidRecords, { nullable: true }) + @UseGuards(WorkspaceAuthGuard) + async checkCustomDomainValidRecords( + @AuthWorkspace() workspace: Workspace, + ): Promise { + return this.customDomainService.checkCustomDomainValidRecords(workspace); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/dtos/custom-domain-valid-records.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/dtos/custom-domain-valid-records.ts index 296f6501c3..9e4e981ef0 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/dtos/custom-domain-valid-records.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/dtos/custom-domain-valid-records.ts @@ -3,17 +3,17 @@ import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() class CustomDomainRecord { @Field(() => String) - validationType: 'ownership' | 'ssl' | 'redirection'; + validationType: 'ssl' | 'redirection'; @Field(() => String) - type: 'txt' | 'cname'; - - @Field(() => String) - key: string; + type: 'cname'; @Field(() => String) status: string; + @Field(() => String) + key: string; + @Field(() => String) value: string; } diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/services/custom-domain.service.spec.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/services/custom-domain.service.spec.ts index 22978fd6f9..30a76a052e 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/services/custom-domain.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/services/custom-domain.service.spec.ts @@ -1,4 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; import Cloudflare from 'cloudflare'; import { CustomHostnameCreateResponse } from 'cloudflare/resources/custom-hostnames/custom-hostnames'; @@ -9,6 +10,7 @@ import { DomainManagerException } from 'src/engine/core-modules/domain-manager/d import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; jest.mock('cloudflare'); @@ -39,6 +41,12 @@ describe('CustomDomainService', () => { getBaseUrl: jest.fn(), }, }, + { + provide: getRepositoryToken(Workspace, 'core'), + useValue: { + save: jest.fn(), + }, + }, ], }).compile(); @@ -62,7 +70,12 @@ describe('CustomDomainService', () => { jest.spyOn(twentyConfigService, 'get').mockReturnValue(mockApiKey); - const instance = new CustomDomainService(twentyConfigService, {} as any); + const instance = new CustomDomainService( + twentyConfigService, + {} as any, + {} as any, + {} as any, + ); expect(twentyConfigService.get).toHaveBeenCalledWith('CLOUDFLARE_API_KEY'); expect(Cloudflare).toHaveBeenCalledWith({ apiToken: mockApiKey }); @@ -138,6 +151,9 @@ describe('CustomDomainService', () => { hostname: customDomain, ownership_verification: undefined, verification_errors: [], + ssl: { + dcv_delegation_records: [], + }, }; const cloudflareMock = { customHostnames: { @@ -284,26 +300,4 @@ describe('CustomDomainService', () => { ).resolves.toBeUndefined(); }); }); - - describe('isCustomDomainWorking', () => { - it('should return true if all records have success status', () => { - const customDomainDetails = { - records: [{ status: 'success' }, { status: 'success' }], - } as any; - - expect( - customDomainService.isCustomDomainWorking(customDomainDetails), - ).toBe(true); - }); - - it('should return false if any record does not have success status', () => { - const customDomainDetails = { - records: [{ status: 'success' }, { status: 'pending' }], - } as any; - - expect( - customDomainService.isCustomDomainWorking(customDomainDetails), - ).toBe(false); - }); - }); }); diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/services/custom-domain.service.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/services/custom-domain.service.ts index ebd4ae159e..f8856289ce 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/services/custom-domain.service.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/services/custom-domain.service.ts @@ -1,8 +1,11 @@ /* @license Enterprise */ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; import Cloudflare from 'cloudflare'; import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; +import { CustomHostnameCreateParams } from 'cloudflare/resources/custom-hostnames/custom-hostnames'; import { DomainManagerException, @@ -12,6 +15,10 @@ import { CustomDomainValidRecords } from 'src/engine/core-modules/domain-manager import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { domainManagerValidator } from 'src/engine/core-modules/domain-manager/validator/cloudflare.validate'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated'; +import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated'; +import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; @Injectable() export class CustomDomainService { @@ -20,6 +27,9 @@ export class CustomDomainService { constructor( private readonly twentyConfigService: TwentyConfigService, private readonly domainManagerService: DomainManagerService, + private readonly auditService: AuditService, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, ) { if (this.twentyConfigService.get('CLOUDFLARE_API_KEY')) { this.cloudflareClient = new Cloudflare({ @@ -28,6 +38,22 @@ export class CustomDomainService { } } + private get sslParams(): CustomHostnameCreateParams['ssl'] { + return { + method: 'txt', + type: 'dv', + settings: { + http2: 'on', + min_tls_version: '1.2', + tls_1_3: 'on', + ciphers: ['ECDHE-RSA-AES128-GCM-SHA256', 'AES128-SHA'], + early_hints: 'on', + }, + bundle_method: 'ubiquitous', + wildcard: false, + }; + } + async registerCustomDomain(customDomain: string) { domainManagerValidator.isCloudflareInstanceDefined(this.cloudflareClient); @@ -35,25 +61,14 @@ export class CustomDomainService { throw new DomainManagerException( 'Hostname already registered', DomainManagerExceptionCode.HOSTNAME_ALREADY_REGISTERED, + { userFriendlyMessage: 'Hostname already registered' }, ); } return await this.cloudflareClient.customHostnames.create({ zone_id: this.twentyConfigService.get('CLOUDFLARE_ZONE_ID'), hostname: customDomain, - ssl: { - method: 'txt', - type: 'dv', - settings: { - http2: 'on', - min_tls_version: '1.2', - tls_1_3: 'on', - ciphers: ['ECDHE-RSA-AES128-GCM-SHA256', 'AES128-SHA'], - early_hints: 'on', - }, - bundle_method: 'ubiquitous', - wildcard: false, - }, + ssl: this.sslParams, }); } @@ -72,70 +87,45 @@ export class CustomDomainService { } if (response.result.length === 1) { + const { hostname, id, ssl, verification_errors, created_at } = + response.result[0]; + // @ts-expect-error - type definition doesn't reflect the real API + const dcvRecords = ssl?.dcv_delegation_records?.[0]; + return { - id: response.result[0].id, - customDomain: response.result[0].hostname, + id: id, + customDomain: hostname, records: [ - response.result[0].ownership_verification, - ...(response.result[0].ssl?.validation_records ?? []), - ] - .map((record) => { - if (!record) return; - - if ( - 'txt_name' in record && - 'txt_value' in record && - record.txt_name && - record.txt_value - ) { - return { - validationType: 'ssl' as const, - type: 'txt' as const, - status: - !response.result[0].ssl.status || - response.result[0].ssl.status.startsWith('pending') - ? 'pending' - : response.result[0].ssl.status, - key: record.txt_name, - value: record.txt_value, - }; - } - - if ( - 'type' in record && - record.type === 'txt' && - record.value && - record.name - ) { - return { - validationType: 'ownership' as const, - type: 'txt' as const, - status: response.result[0].status ?? 'pending', - key: record.name, - value: record.value, - }; - } - }) - .filter(isDefined) - .concat([ - { - validationType: 'redirection' as const, - type: 'cname' as const, - status: - // wait 10s before starting the real check - response.result[0].created_at && - new Date().getTime() - - new Date(response.result[0].created_at).getTime() < - 1000 * 10 - ? 'pending' - : response.result[0].verification_errors?.[0] === - 'custom hostname does not CNAME to this zone.' - ? 'error' - : 'success', - key: response.result[0].hostname, - value: this.domainManagerService.getBaseUrl().hostname, - }, - ]), + { + validationType: 'redirection' as const, + type: 'cname', + status: + // wait 10s before starting the real check + created_at && + new Date().getTime() - new Date(created_at).getTime() < 1000 * 10 + ? 'pending' + : verification_errors?.[0] === + 'custom hostname does not CNAME to this zone.' + ? 'error' + : 'success', + key: hostname, + value: this.domainManagerService.getBaseUrl().hostname, + }, + { + validationType: 'ssl' as const, + type: 'cname', + status: + !ssl.status || ssl.status.startsWith('pending') + ? 'pending' + : ssl.status === 'active' + ? 'success' + : ssl.status, + key: dcvRecords?.cname ?? `_acme-challenge.${hostname}`, + value: + dcvRecords?.cname_target ?? + `${hostname}.${this.twentyConfigService.get('CLOUDFLARE_DCV_DELEGATION_ID')}.dcv.cloudflare.com`, + }, + ], }; } @@ -179,9 +169,48 @@ export class CustomDomainService { }); } - isCustomDomainWorking(customDomainDetails: CustomDomainValidRecords) { - return customDomainDetails.records.every( - ({ status }) => status === 'success', + private async refreshCustomDomain( + customDomainDetails: CustomDomainValidRecords, + ) { + domainManagerValidator.isCloudflareInstanceDefined(this.cloudflareClient); + + await this.cloudflareClient.customHostnames.edit(customDomainDetails.id, { + zone_id: this.twentyConfigService.get('CLOUDFLARE_ZONE_ID'), + ssl: this.sslParams, + }); + } + + async checkCustomDomainValidRecords(workspace: Workspace) { + if (!workspace.customDomain) return; + + const customDomainDetails = await this.getCustomDomainDetails( + workspace.customDomain, ); + + if (!customDomainDetails) return; + + await this.refreshCustomDomain(customDomainDetails); + + const isCustomDomainWorking = + this.domainManagerService.isCustomDomainWorking(customDomainDetails); + + if (workspace.isCustomDomainEnabled !== isCustomDomainWorking) { + workspace.isCustomDomainEnabled = isCustomDomainWorking; + + await this.workspaceRepository.save(workspace); + + const analytics = this.auditService.createContext({ + workspaceId: workspace.id, + }); + + analytics.insertWorkspaceEvent( + workspace.isCustomDomainEnabled + ? CUSTOM_DOMAIN_ACTIVATED_EVENT + : CUSTOM_DOMAIN_DEACTIVATED_EVENT, + {}, + ); + } + + return customDomainDetails; } } diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/validator/cloudflare.validate.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/validator/cloudflare.validate.ts index 2926e285c1..0ea9f03a6e 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/validator/cloudflare.validate.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/validator/cloudflare.validate.ts @@ -1,4 +1,5 @@ import Cloudflare from 'cloudflare'; +import { t } from '@lingui/core/macro'; import { DomainManagerException, @@ -12,6 +13,9 @@ const isCloudflareInstanceDefined = ( throw new DomainManagerException( 'Cloudflare instance is not defined', DomainManagerExceptionCode.CLOUDFLARE_CLIENT_NOT_INITIALIZED, + { + userFriendlyMessage: t`Environnement variable CLOUDFLARE_API_KEY must be defined to use this feature.`, + }, ); } }; diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts index 0bb31f6dc0..fef78ac39a 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts @@ -1,15 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class EmailVerificationException extends CustomException { - declare code: EmailVerificationExceptionCode; - constructor( - message: string, - code: EmailVerificationExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage ?? message); - } -} +export class EmailVerificationException extends CustomException {} export enum EmailVerificationExceptionCode { EMAIL_VERIFICATION_NOT_REQUIRED = 'EMAIL_VERIFICATION_NOT_REQUIRED', diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.exception.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.exception.ts index b02bfe7278..5c32d9a222 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.exception.ts @@ -1,13 +1,7 @@ import { CustomException } from 'src/utils/custom-exception'; -export class FeatureFlagException extends CustomException { - constructor(message: string, code: FeatureFlagExceptionCode) { - super(message, code); - } -} +export class FeatureFlagException extends CustomException {} export enum FeatureFlagExceptionCode { INVALID_FEATURE_FLAG_KEY = 'INVALID_FEATURE_FLAG_KEY', - FEATURE_FLAG_IS_NOT_PUBLIC = 'FEATURE_FLAG_IS_NOT_PUBLIC', - FEATURE_FLAG_NOT_FOUND = 'FEATURE_FLAG_NOT_FOUND', } diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.spec.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.spec.ts index ce8c9e99b0..21213ddaee 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.spec.ts @@ -1,5 +1,5 @@ import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate'; -import { CustomException } from 'src/utils/custom-exception'; +import { UnknownException } from 'src/utils/custom-exception'; describe('featureFlagValidator', () => { describe('assertIsFeatureFlagKey', () => { @@ -7,7 +7,7 @@ describe('featureFlagValidator', () => { expect(() => featureFlagValidator.assertIsFeatureFlagKey( 'IS_AI_ENABLED', - new CustomException('Error', 'Error'), + new UnknownException('Error', 'Error'), ), ).not.toThrow(); }); @@ -16,14 +16,14 @@ describe('featureFlagValidator', () => { expect(() => featureFlagValidator.assertIsFeatureFlagKey( 'IS_WORKFLOW_FILTERING_ENABLED', - new CustomException('Error', 'Error'), + new UnknownException('Error', 'Error'), ), ).not.toThrow(); }); it('should throw error if featureFlagKey is invalid', () => { const invalidKey = 'InvalidKey'; - const exception = new CustomException('Error', 'Error'); + const exception = new UnknownException('Error', 'Error'); expect(() => featureFlagValidator.assertIsFeatureFlagKey(invalidKey, exception), diff --git a/packages/twenty-server/src/engine/core-modules/file/crons/jobs/cleanup-orphaned-files.cron.job.ts b/packages/twenty-server/src/engine/core-modules/file/crons/jobs/cleanup-orphaned-files.cron.job.ts index 47f4e055fd..993ce72905 100644 --- a/packages/twenty-server/src/engine/core-modules/file/crons/jobs/cleanup-orphaned-files.cron.job.ts +++ b/packages/twenty-server/src/engine/core-modules/file/crons/jobs/cleanup-orphaned-files.cron.job.ts @@ -4,7 +4,6 @@ import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { IsNull, LessThan, Repository } from 'typeorm'; import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator'; -import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { FileMetadataService } from 'src/engine/core-modules/file/services/file-metadata.service'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; @@ -22,7 +21,6 @@ export class CleanupOrphanedFilesCronJob { @InjectRepository(FileEntity, 'core') private readonly fileRepository: Repository, private readonly fileMetadataService: FileMetadataService, - private readonly exceptionHandlerService: ExceptionHandlerService, ) {} @Process(CleanupOrphanedFilesCronJob.name) diff --git a/packages/twenty-server/src/engine/core-modules/file/file.exception.ts b/packages/twenty-server/src/engine/core-modules/file/file.exception.ts index d5d98869eb..782eda9519 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file.exception.ts @@ -1,13 +1,13 @@ -import { CustomException } from 'src/utils/custom-exception'; +import { + appendCommonExceptionCode, + CustomException, +} from 'src/utils/custom-exception'; -export enum FileExceptionCode { - UNAUTHENTICATED = 'UNAUTHENTICATED', - INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', - FILE_NOT_FOUND = 'FILE_NOT_FOUND', -} +export class FileException extends CustomException< + keyof typeof FileExceptionCode +> {} -export class FileException extends CustomException { - constructor(message: string, code: FileExceptionCode) { - super(message, code); - } -} +export const FileExceptionCode = appendCommonExceptionCode({ + UNAUTHENTICATED: 'UNAUTHENTICATED', + FILE_NOT_FOUND: 'FILE_NOT_FOUND', +} as const); diff --git a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts index 1b67fe2aaa..3ff112ac17 100644 --- a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts +++ b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts @@ -170,42 +170,6 @@ export class ForbiddenError extends BaseGraphQLError { } } -export class PersistedQueryNotFoundError extends BaseGraphQLError { - constructor(customException: CustomException); - - constructor(message?: string, extensions?: RestrictedGraphQLErrorExtensions); - - constructor( - messageOrException?: string | CustomException, - extensions?: RestrictedGraphQLErrorExtensions, - ) { - super( - messageOrException || 'PersistedQueryNotFound', - ErrorCode.PERSISTED_QUERY_NOT_FOUND, - extensions, - ); - Object.defineProperty(this, 'name', { - value: 'PersistedQueryNotFoundError', - }); - } -} - -export class PersistedQueryNotSupportedError extends BaseGraphQLError { - constructor( - messageOrException?: string | CustomException, - extensions?: RestrictedGraphQLErrorExtensions, - ) { - super( - messageOrException || 'PersistedQueryNotSupported', - ErrorCode.PERSISTED_QUERY_NOT_SUPPORTED, - extensions, - ); - Object.defineProperty(this, 'name', { - value: 'PersistedQueryNotSupportedError', - }); - } -} - export class UserInputError extends BaseGraphQLError { constructor(exception: CustomException); diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/record-transformer.exception.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/record-transformer.exception.ts index a7dfc7acf4..531c697f41 100644 --- a/packages/twenty-server/src/engine/core-modules/record-transformer/record-transformer.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/record-transformer.exception.ts @@ -1,15 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class RecordTransformerException extends CustomException { - declare code: RecordTransformerExceptionCode; - constructor( - message: string, - code: RecordTransformerExceptionCode, - userFriendlyMessage?: string, - ) { - super(message, code, userFriendlyMessage); - } -} +export class RecordTransformerException extends CustomException {} export enum RecordTransformerExceptionCode { INVALID_URL = 'INVALID_URL', diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts index 33973cf331..f3323fdd3b 100644 --- a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts @@ -43,7 +43,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({ throw new RecordTransformerException( `Invalid country code ${countryCode}`, RecordTransformerExceptionCode.INVALID_PHONE_COUNTRY_CODE, - t`Invalid country code ${countryCode}`, + { userFriendlyMessage: t`Invalid country code ${countryCode}` }, ); } @@ -57,7 +57,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({ throw new RecordTransformerException( `Invalid calling code ${callingCode}`, RecordTransformerExceptionCode.INVALID_PHONE_CALLING_CODE, - t`Invalid calling code ${callingCode}`, + { userFriendlyMessage: t`Invalid calling code ${callingCode}` }, ); } @@ -70,7 +70,9 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({ throw new RecordTransformerException( `Provided country code and calling code are conflicting`, RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE_AND_COUNTRY_CODE, - t`Provided country code and calling code are conflicting`, + { + userFriendlyMessage: t`Provided country code and calling code are conflicting`, + }, ); } }; @@ -91,7 +93,7 @@ const parsePhoneNumberExceptionWrapper = ({ throw new RecordTransformerException( `Provided phone number is invalid ${number}`, RecordTransformerExceptionCode.INVALID_PHONE_NUMBER, - t`Provided phone number is invalid ${number}`, + { userFriendlyMessage: t`Provided phone number is invalid ${number}` }, ); } }; @@ -115,7 +117,9 @@ const validateAndInferMetadataFromPrimaryPhoneNumber = ({ throw new RecordTransformerException( 'Provided and inferred country code are conflicting', RecordTransformerExceptionCode.CONFLICTING_PHONE_COUNTRY_CODE, - t`Provided and inferred country code are conflicting`, + { + userFriendlyMessage: t`Provided and inferred country code are conflicting`, + }, ); } @@ -127,7 +131,9 @@ const validateAndInferMetadataFromPrimaryPhoneNumber = ({ throw new RecordTransformerException( 'Provided and inferred calling code are conflicting', RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE, - t`Provided and inferred calling code are conflicting`, + { + userFriendlyMessage: t`Provided and inferred calling code are conflicting`, + }, ); } diff --git a/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts b/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts index 97603519b1..78b84504f7 100644 --- a/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts @@ -1,11 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class SearchException extends CustomException { - declare code: SearchExceptionCode; - constructor(message: string, code: SearchExceptionCode) { - super(message, code); - } -} +export class SearchException extends CustomException {} export enum SearchExceptionCode { LABEL_IDENTIFIER_FIELD_NOT_FOUND = 'LABEL_IDENTIFIER_FIELD_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.exception.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.exception.ts index d32ce9e110..c9b94f6a4f 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/sso.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.exception.ts @@ -2,15 +2,10 @@ import { CustomException } from 'src/utils/custom-exception'; -export class SSOException extends CustomException { - constructor(message: string, code: SSOExceptionCode) { - super(message, code); - } -} +export class SSOException extends CustomException {} export enum SSOExceptionCode { USER_NOT_FOUND = 'USER_NOT_FOUND', - INVALID_SSO_CONFIGURATION = 'INVALID_SSO_CONFIGURATION', IDENTITY_PROVIDER_NOT_FOUND = 'IDENTITY_PROVIDER_NOT_FOUND', INVALID_ISSUER_URL = 'INVALID_ISSUER_URL', INVALID_IDP_TYPE = 'INVALID_IDP_TYPE', diff --git a/packages/twenty-server/src/engine/core-modules/throttler/throttler.exception.ts b/packages/twenty-server/src/engine/core-modules/throttler/throttler.exception.ts index ea94c5d2ec..753a075b7a 100644 --- a/packages/twenty-server/src/engine/core-modules/throttler/throttler.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/throttler/throttler.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class ThrottlerException extends CustomException { - constructor(message: string, code: ThrottlerExceptionCode) { - super(message, code); - } -} +export class ThrottlerException extends CustomException {} export enum ThrottlerExceptionCode { LIMIT_REACHED = 'LIMIT_REACHED', diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts index 75824fcca9..a2f56702c6 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/exceptions/send-email-tool.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class SendEmailToolException extends CustomException { - constructor(message: string, code: SendEmailToolExceptionCode) { - super(message, code); - } -} +export class SendEmailToolException extends CustomException {} export enum SendEmailToolExceptionCode { INVALID_CONNECTED_ACCOUNT_ID = 'INVALID_CONNECTED_ACCOUNT_ID', diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts index 79e8f63e94..92c5227d74 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts @@ -972,6 +972,15 @@ export class ConfigVariables { @IsOptional() CLOUDFLARE_WEBHOOK_SECRET: string; + @ConfigVariablesMetadata({ + group: ConfigVariablesGroup.Other, + description: + 'Id to generate value for CNAME record to validate ownership and manage ssl for custom hostname with Cloudflare', + type: ConfigVariableType.STRING, + }) + @IsOptional() + CLOUDFLARE_DCV_DELEGATION_ID: string; + @ConfigVariablesMetadata({ group: ConfigVariablesGroup.LLM, description: diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/twenty-config.exception.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/twenty-config.exception.ts index dd24b7da71..4c0d09b2f9 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/twenty-config.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/twenty-config.exception.ts @@ -1,11 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class ConfigVariableException extends CustomException { - declare code: ConfigVariableExceptionCode; - constructor(message: string, code: ConfigVariableExceptionCode) { - super(message, code); - } -} +export class ConfigVariableException extends CustomException {} export enum ConfigVariableExceptionCode { DATABASE_CONFIG_DISABLED = 'DATABASE_CONFIG_DISABLED', diff --git a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.exception.ts b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.exception.ts index 296ed1ddb0..059040b75f 100644 --- a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.exception.ts @@ -1,11 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class TwoFactorAuthenticationException extends CustomException { - declare code: TwoFactorAuthenticationExceptionCode; - constructor(message: string, code: TwoFactorAuthenticationExceptionCode) { - super(message, code); - } -} +export class TwoFactorAuthenticationException extends CustomException {} export enum TwoFactorAuthenticationExceptionCode { INVALID_CONFIGURATION = 'INVALID_CONFIGURATION', diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.exception.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.exception.ts index 116bb089f3..c66b14a3c4 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.exception.ts @@ -1,11 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class UserWorkspaceException extends CustomException { - declare code: UserWorkspaceExceptionCode; - constructor(message: string, code: UserWorkspaceExceptionCode) { - super(message, code); - } -} +export class UserWorkspaceException extends CustomException {} export enum UserWorkspaceExceptionCode { USER_WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/core-modules/user/user.exception.ts b/packages/twenty-server/src/engine/core-modules/user/user.exception.ts index f67c6bc3d3..39363339c2 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class UserException extends CustomException { - constructor(message: string, code: UserExceptionCode) { - super(message, code); - } -} +export class UserException extends CustomException {} export enum UserExceptionCode { USER_NOT_FOUND = 'USER_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/core-modules/webhook/webhook.exception.ts b/packages/twenty-server/src/engine/core-modules/webhook/webhook.exception.ts index bd49220efe..d3526ecce5 100644 --- a/packages/twenty-server/src/engine/core-modules/webhook/webhook.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/webhook/webhook.exception.ts @@ -1,15 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WebhookException extends CustomException { - declare code: WebhookExceptionCode; - constructor( - message: string, - code: WebhookExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class WebhookException extends CustomException {} export enum WebhookExceptionCode { WEBHOOK_NOT_FOUND = 'WEBHOOK_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.exception.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.exception.ts index e52ad66916..1f33743bbb 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkspaceInvitationException extends CustomException { - constructor(message: string, code: WorkspaceInvitationExceptionCode) { - super(message, code); - } -} +export class WorkspaceInvitationException extends CustomException {} export enum WorkspaceInvitationExceptionCode { INVALID_APP_TOKEN_TYPE = 'INVALID_APP_TOKEN_TYPE', diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index 23a2e4ec8b..9da8a9073d 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -8,14 +8,10 @@ import { isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { Repository } from 'typeorm'; -import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; -import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated'; -import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated'; import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { @@ -67,10 +63,8 @@ export class WorkspaceService extends TypeOrmQueryService { private readonly billingService: BillingService, private readonly userWorkspaceService: UserWorkspaceService, private readonly twentyConfigService: TwentyConfigService, - private readonly domainManagerService: DomainManagerService, private readonly exceptionHandlerService: ExceptionHandlerService, private readonly permissionsService: PermissionsService, - private readonly auditService: AuditService, private readonly customDomainService: CustomDomainService, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, @InjectMessageQueue(MessageQueue.deleteCascadeQueue) @@ -182,6 +176,7 @@ export class WorkspaceService extends TypeOrmQueryService { await this.customDomainService.deleteCustomHostnameByHostnameSilently( workspace.customDomain, ); + workspace.isCustomDomainEnabled = false; } if ( @@ -401,38 +396,6 @@ export class WorkspaceService extends TypeOrmQueryService { return !existingWorkspace; } - async checkCustomDomainValidRecords(workspace: Workspace) { - if (!workspace.customDomain) return; - - const customDomainDetails = - await this.customDomainService.getCustomDomainDetails( - workspace.customDomain, - ); - - if (!customDomainDetails) return; - - const isCustomDomainWorking = - this.domainManagerService.isCustomDomainWorking(customDomainDetails); - - if (workspace.isCustomDomainEnabled !== isCustomDomainWorking) { - workspace.isCustomDomainEnabled = isCustomDomainWorking; - await this.workspaceRepository.save(workspace); - - const analytics = this.auditService.createContext({ - workspaceId: workspace.id, - }); - - analytics.insertWorkspaceEvent( - workspace.isCustomDomainEnabled - ? CUSTOM_DOMAIN_ACTIVATED_EVENT - : CUSTOM_DOMAIN_DEACTIVATED_EVENT, - {}, - ); - } - - return customDomainDetails; - } - private async validateSecurityPermissions({ payload, userWorkspaceId, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/utils/__tests__/workspace-graphql-api-exception-handler.util.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/utils/__tests__/workspace-graphql-api-exception-handler.util.spec.ts index 26e94f054e..8105c4f5d9 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/utils/__tests__/workspace-graphql-api-exception-handler.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/utils/__tests__/workspace-graphql-api-exception-handler.util.spec.ts @@ -7,7 +7,6 @@ import { WorkspaceException, WorkspaceExceptionCode, } from 'src/engine/core-modules/workspace/workspace.exception'; -import { CustomException } from 'src/utils/custom-exception'; describe('workspaceGraphqlApiExceptionHandler', () => { it('should throw NotFoundError when WorkspaceExceptionCode is SUBDOMAIN_NOT_FOUND', () => { @@ -48,7 +47,7 @@ describe('workspaceGraphqlApiExceptionHandler', () => { const error = new WorkspaceException('Unknown error', 'UNKNOWN_CODE'); expect(() => workspaceGraphqlApiExceptionHandler(error)).toThrow( - CustomException, + WorkspaceException, ); }); diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts index 29f6c421b7..1d3151b696 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts @@ -1,11 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkspaceException extends CustomException { - declare code: WorkspaceExceptionCode; - constructor(message: string, code: WorkspaceExceptionCode) { - super(message, code); - } -} +export class WorkspaceException extends CustomException {} export enum WorkspaceExceptionCode { SUBDOMAIN_NOT_FOUND = 'SUBDOMAIN_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts index 2b7cf70f47..678c8cba4a 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts @@ -5,11 +5,9 @@ import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; -import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; @@ -26,6 +24,7 @@ import { RoleModule } from 'src/engine/metadata-modules/role/role.module'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; +import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; import { Workspace } from './workspace.entity'; @@ -38,7 +37,6 @@ import { WorkspaceService } from './services/workspace.service'; TypeOrmModule.forFeature([BillingSubscription], 'core'), NestjsQueryGraphQLModule.forFeature({ imports: [ - DomainManagerModule, BillingModule, FileModule, TokenModule, @@ -56,9 +54,9 @@ import { WorkspaceService } from './services/workspace.service'; TypeORMModule, PermissionsModule, WorkspaceCacheStorageModule, - AuditModule, RoleModule, AgentModule, + DomainManagerModule, ], services: [WorkspaceService], resolvers: workspaceAutoResolverOpts, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index ce2e6f9659..b21b1bcbe5 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -25,7 +25,6 @@ import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder. import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; -import { CustomDomainValidRecords } from 'src/engine/core-modules/domain-manager/dtos/custom-domain-valid-records'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { FeatureFlagDTO } from 'src/engine/core-modules/feature-flag/dtos/feature-flag-dto'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @@ -70,7 +69,7 @@ import { Workspace } from './workspace.entity'; import { WorkspaceService } from './services/workspace.service'; const OriginHeader = createParamDecorator( - (data: unknown, ctx: ExecutionContext) => { + (_: unknown, ctx: ExecutionContext) => { const request = getRequest(ctx); return request.headers['origin']; @@ -318,14 +317,6 @@ export class WorkspaceResolver { ); } - @Mutation(() => CustomDomainValidRecords, { nullable: true }) - @UseGuards(WorkspaceAuthGuard) - async checkCustomDomainValidRecords( - @AuthWorkspace() workspace: Workspace, - ): Promise { - return this.workspaceService.checkCustomDomainValidRecords(workspace); - } - @Query(() => PublicWorkspaceDataOutput) @UseGuards(PublicEndpointGuard) async getPublicWorkspaceDataByDomain( diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent.exception.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent.exception.ts index f1e2def599..1d8990129b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent.exception.ts @@ -1,11 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class AgentException extends CustomException { - declare code: AgentExceptionCode; - constructor(message: string, code: AgentExceptionCode) { - super(message, code); - } -} +export class AgentException extends CustomException {} export enum AgentExceptionCode { AGENT_NOT_FOUND = 'AGENT_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.exception.ts b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.exception.ts index 89672fa26a..33bbb01e5d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/data-source/data-source.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class DataSourceException extends CustomException { - constructor(message: string, code: DataSourceExceptionCode) { - super(message, code); - } -} +export class DataSourceException extends CustomException {} export enum DataSourceExceptionCode { DATA_SOURCE_NOT_FOUND = 'DATA_SOURCE_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts index b22b43c40f..72bd3f94a1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts @@ -1,25 +1,22 @@ -import { CustomException } from 'src/utils/custom-exception'; +import { + appendCommonExceptionCode, + CustomException, +} from 'src/utils/custom-exception'; -export class FieldMetadataException extends CustomException { - declare code: FieldMetadataExceptionCode; - constructor( - message: string, - code: FieldMetadataExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class FieldMetadataException extends CustomException< + keyof typeof FieldMetadataExceptionCode +> {} -export enum FieldMetadataExceptionCode { - FIELD_METADATA_NOT_FOUND = 'FIELD_METADATA_NOT_FOUND', - INVALID_FIELD_INPUT = 'INVALID_FIELD_INPUT', - FIELD_MUTATION_NOT_ALLOWED = 'FIELD_MUTATION_NOT_ALLOWED', - FIELD_ALREADY_EXISTS = 'FIELD_ALREADY_EXISTS', - OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', - INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', - FIELD_METADATA_RELATION_NOT_ENABLED = 'FIELD_METADATA_RELATION_NOT_ENABLED', - FIELD_METADATA_RELATION_MALFORMED = 'FIELD_METADATA_RELATION_MALFORMED', - LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND = 'LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND', - UNCOVERED_FIELD_METADATA_TYPE_VALIDATION = 'UNCOVERED_FIELD_METADATA_TYPE_VALIDATION', -} +export const FieldMetadataExceptionCode = appendCommonExceptionCode({ + FIELD_METADATA_NOT_FOUND: 'FIELD_METADATA_NOT_FOUND', + INVALID_FIELD_INPUT: 'INVALID_FIELD_INPUT', + FIELD_MUTATION_NOT_ALLOWED: 'FIELD_MUTATION_NOT_ALLOWED', + FIELD_ALREADY_EXISTS: 'FIELD_ALREADY_EXISTS', + OBJECT_METADATA_NOT_FOUND: 'OBJECT_METADATA_NOT_FOUND', + FIELD_METADATA_RELATION_NOT_ENABLED: 'FIELD_METADATA_RELATION_NOT_ENABLED', + FIELD_METADATA_RELATION_MALFORMED: 'FIELD_METADATA_RELATION_MALFORMED', + LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND: + 'LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND', + UNCOVERED_FIELD_METADATA_TYPE_VALIDATION: + 'UNCOVERED_FIELD_METADATA_TYPE_VALIDATION', +} as const); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts index cffd002865..ffaa7e19e9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts @@ -1,15 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class ObjectMetadataException extends CustomException { - declare code: ObjectMetadataExceptionCode; - constructor( - message: string, - code: ObjectMetadataExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class ObjectMetadataException extends CustomException {} export enum ObjectMetadataExceptionCode { OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts index e877fd359e..d4082d3b70 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts @@ -1,15 +1,12 @@ import { CustomException } from 'src/utils/custom-exception'; -export class PermissionsException extends CustomException { - declare code: PermissionsExceptionCode; - constructor( - message: string, - code: PermissionsExceptionCode, - userFriendlyMessage?: string, - ) { - super(message, code, userFriendlyMessage); - } -} +// TODO: It would be usefull to enable typed message like below. More refactorisation is necessary to use it. +// export class PermissionsException extends CustomException< +// PermissionsExceptionCode, +// false, +// PermissionsExceptionMessage +// > {} +export class PermissionsException extends CustomException {} export enum PermissionsExceptionCode { PERMISSION_DENIED = 'PERMISSION_DENIED', @@ -54,17 +51,11 @@ export enum PermissionsExceptionCode { export enum PermissionsExceptionMessage { PERMISSION_DENIED = 'User does not have permission', - ADMIN_ROLE_NOT_FOUND = 'Admin role not found', USER_WORKSPACE_NOT_FOUND = 'User workspace not found', - WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH = 'Workspace id role user workspace mismatch', - TOO_MANY_ADMIN_CANDIDATES = 'Too many admin candidates', - USER_WORKSPACE_ALREADY_HAS_ROLE = 'User workspace already has role', - WORKSPACE_MEMBER_NOT_FOUND = 'Workspace member not found', ROLE_NOT_FOUND = 'Role not found', CANNOT_UNASSIGN_LAST_ADMIN = 'Cannot unassign admin role from last admin of the workspace', CANNOT_DELETE_LAST_ADMIN_USER = 'Cannot delete account: user is the unique admin of a workspace', UNKNOWN_OPERATION_NAME = 'Unknown operation name, cannot determine required permission', - UNKNOWN_REQUIRED_PERMISSION = 'Unknown required permission', CANNOT_UPDATE_SELF_ROLE = 'Cannot update self role', NO_ROLE_FOUND_FOR_USER_WORKSPACE = 'No role found for userWorkspace', ROLE_LABEL_ALREADY_EXISTS = 'A role with this label already exists', @@ -73,7 +64,6 @@ export enum PermissionsExceptionMessage { INVALID_SETTING = 'Invalid permission setting (unknown value)', ROLE_NOT_EDITABLE = 'Role is not editable', DEFAULT_ROLE_CANNOT_BE_DELETED = 'Default role cannot be deleted', - NO_PERMISSIONS_FOUND_IN_DATASOURCE = 'No permissions found in datasource', CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT = 'Cannot add object permission on system object', CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT = 'Cannot add field permission on system object', CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT = 'Cannot give update permission to non-readable object', @@ -82,8 +72,6 @@ export enum PermissionsExceptionMessage { ONLY_FIELD_RESTRICTION_ALLOWED = 'Field permission can only introduce a restriction', FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT = 'Field restriction only makes sense on readable object', FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT = 'Field restriction on update only makes sense on updatable object', - UPSERT_FIELD_PERMISSION_FAILED = 'Failed to upsert field permission', - PERMISSION_NOT_FOUND = 'Permission not found', OBJECT_PERMISSION_NOT_FOUND = 'Object permission not found', EMPTY_FIELD_PERMISSION_NOT_ALLOWED = 'Empty field permission not allowed', } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.exception.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.exception.ts index ab95b6e4df..ada14e709b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.exception.ts @@ -1,11 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class RemoteServerException extends CustomException { - declare code: RemoteServerExceptionCode; - constructor(message: string, code: RemoteServerExceptionCode) { - super(message, code); - } -} +export class RemoteServerException extends CustomException {} export enum RemoteServerExceptionCode { REMOTE_SERVER_NOT_FOUND = 'REMOTE_SERVER_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.exception.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.exception.ts index f446f318f1..821a3f4f99 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.exception.ts @@ -1,12 +1,12 @@ -import { CustomException } from 'src/utils/custom-exception'; +import { + appendCommonExceptionCode, + CustomException, +} from 'src/utils/custom-exception'; -export class DistantTableException extends CustomException { - constructor(message: string, code: DistantTableExceptionCode) { - super(message, code); - } -} +export class DistantTableException extends CustomException< + keyof typeof DistantTableExceptionCode +> {} -export enum DistantTableExceptionCode { - INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', - TIMEOUT_ERROR = 'TIMEOUT_ERROR', -} +export const DistantTableExceptionCode = appendCommonExceptionCode({ + TIMEOUT_ERROR: 'TIMEOUT_ERROR', +} as const); diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.exception.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.exception.ts index e0be499f5c..8cad5b875d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class ForeignTableException extends CustomException { - constructor(message: string, code: ForeignTableExceptionCode) { - super(message, code); - } -} +export class ForeignTableException extends CustomException {} export enum ForeignTableExceptionCode { FOREIGN_TABLE_MUTATION_NOT_ALLOWED = 'FOREIGN_TABLE_MUTATION_NOT_ALLOWED', diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.exception.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.exception.ts index 37fd452c83..6004a6855d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.exception.ts @@ -1,11 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class RemoteTableException extends CustomException { - declare code: RemoteTableExceptionCode; - constructor(message: string, code: RemoteTableExceptionCode) { - super(message, code); - } -} +export class RemoteTableException extends CustomException {} export enum RemoteTableExceptionCode { REMOTE_TABLE_NOT_FOUND = 'REMOTE_TABLE_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts index b53fd07305..f2002e3cef 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts @@ -292,7 +292,7 @@ export class RoleService { throw new PermissionsException( PermissionsExceptionMessage.ROLE_LABEL_ALREADY_EXISTS, PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS, - t`A role with this label already exists.`, + { userFriendlyMessage: t`A role with this label already exists.` }, ); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts index f05c16c841..1bd95e6836 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts @@ -1,11 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class ServerlessFunctionException extends CustomException { - declare code: ServerlessFunctionExceptionCode; - constructor(message: string, code: ServerlessFunctionExceptionCode) { - super(message, code); - } -} +export class ServerlessFunctionException extends CustomException {} export enum ServerlessFunctionExceptionCode { SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts b/packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts index 4eac9d2f28..8b0c512fc7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts @@ -1,14 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class InvalidMetadataException extends CustomException { - constructor( - message: string, - code: InvalidMetadataExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class InvalidMetadataException extends CustomException {} export enum InvalidMetadataExceptionCode { LABEL_REQUIRED = 'Label required', @@ -16,7 +8,6 @@ export enum InvalidMetadataExceptionCode { EXCEEDS_MAX_LENGTH = 'Exceeds max length', RESERVED_KEYWORD = 'Reserved keyword', NOT_CAMEL_CASE = 'Not camel case', - NOT_FIRST_LETTER_UPPER_CASE = 'Not first letter upper case', INVALID_LABEL = 'Invalid label', NAME_NOT_SYNCED_WITH_LABEL = 'Name not synced with label', INVALID_STRING = 'Invalid string', diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception.ts index 323708b8b2..6e2ef1fdb7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkspaceMetadataCacheException extends CustomException { - constructor(message: string, code: WorkspaceMetadataCacheExceptionCode) { - super(message, code); - } -} +export class WorkspaceMetadataCacheException extends CustomException {} export enum WorkspaceMetadataCacheExceptionCode { OBJECT_METADATA_MAP_NOT_FOUND = 'Object Metadata map not found', diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception.ts index 183446a051..2c9ce4347f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkspaceMetadataVersionException extends CustomException { - constructor(message: string, code: WorkspaceMetadataVersionExceptionCode) { - super(message, code); - } -} +export class WorkspaceMetadataVersionException extends CustomException {} export enum WorkspaceMetadataVersionExceptionCode { METADATA_VERSION_NOT_FOUND = 'METADATA_VERSION_NOT_FOUND', diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.exception.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.exception.ts index 41f6698249..84c2abd1f5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkspaceMigrationException extends CustomException { - constructor(message: string, code: WorkspaceMigrationExceptionCode) { - super(message, code); - } -} +export class WorkspaceMigrationException extends CustomException {} export enum WorkspaceMigrationExceptionCode { NO_FACTORY_FOUND = 'NO_FACTORY_FOUND', diff --git a/packages/twenty-server/src/engine/twenty-orm/exceptions/relation.exception.ts b/packages/twenty-server/src/engine/twenty-orm/exceptions/relation.exception.ts index fc35a947aa..0cdca3e196 100644 --- a/packages/twenty-server/src/engine/twenty-orm/exceptions/relation.exception.ts +++ b/packages/twenty-server/src/engine/twenty-orm/exceptions/relation.exception.ts @@ -1,17 +1,10 @@ import { CustomException } from 'src/utils/custom-exception'; -export class RelationException extends CustomException { - declare code: RelationExceptionCode; - constructor(message: string, code: RelationExceptionCode) { - super(message, code); - } -} +export class RelationException extends CustomException {} export enum RelationExceptionCode { RELATION_OBJECT_METADATA_NOT_FOUND = 'RELATION_OBJECT_METADATA_NOT_FOUND', RELATION_TARGET_FIELD_METADATA_ID_NOT_FOUND = 'RELATION_TARGET_FIELD_METADATA_ID_NOT_FOUND', - RELATION_TARGET_FIELD_METADATA_NOT_FOUND = 'RELATION_TARGET_FIELD_METADATA_NOT_FOUND', - INVALID_RELATION_TYPE = 'INVALID_RELATION_TYPE', RELATION_JOIN_COLUMN_ON_BOTH_SIDES = 'RELATION_JOIN_COLUMN_ON_BOTH_SIDES', MISSING_RELATION_JOIN_COLUMN = 'MISSING_RELATION_JOIN_COLUMN', MULTIPLE_JOIN_COLUMNS_FOUND = 'MULTIPLE_JOIN_COLUMNS_FOUND', diff --git a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts index caf281673e..b2773372d4 100644 --- a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts +++ b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts @@ -1,14 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class TwentyORMException extends CustomException { - constructor( - message: string, - code: TwentyORMExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class TwentyORMException extends CustomException {} export enum TwentyORMExceptionCode { METADATA_VERSION_MISMATCH = 'METADATA_VERSION_MISMATCH', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts index dc523164d6..36c2aa78a8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkspaceCleanerException extends CustomException { - constructor(message: string, code: WorkspaceCleanerExceptionCode) { - super(message, code); - } -} +export class WorkspaceCleanerException extends CustomException {} export enum WorkspaceCleanerExceptionCode { BILLING_SUBSCRIPTION_NOT_FOUND = 'BILLING_SUBSCRIPTION_NOT_FOUND', diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts index 6a78aafaef..f4c6c1b8ad 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class CalendarEventImportDriverException extends CustomException { - constructor(message: string, code: CalendarEventImportDriverExceptionCode) { - super(message, code); - } -} +export class CalendarEventImportDriverException extends CustomException {} export enum CalendarEventImportDriverExceptionCode { NOT_FOUND = 'NOT_FOUND', diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/exceptions/calendar-event-import.exception.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/exceptions/calendar-event-import.exception.ts index 7219920e53..07c02993ee 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/exceptions/calendar-event-import.exception.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/exceptions/calendar-event-import.exception.ts @@ -1,13 +1,8 @@ import { CustomException } from 'src/utils/custom-exception'; -export class CalendarEventImportException extends CustomException { - constructor(message: string, code: CalendarEventImportExceptionCode) { - super(message, code); - } -} +export class CalendarEventImportException extends CustomException {} export enum CalendarEventImportExceptionCode { PROVIDER_NOT_SUPPORTED = 'PROVIDER_NOT_SUPPORTED', UNKNOWN = 'UNKNOWN', - CALENDAR_CHANNEL_NOT_FOUND = 'CALENDAR_CHANNEL_NOT_FOUND', } diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception.ts index dc1244c40d..86d225b744 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception.ts @@ -1,13 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class ConnectedAccountRefreshAccessTokenException extends CustomException { - constructor( - message: string, - code: ConnectedAccountRefreshAccessTokenExceptionCode, - ) { - super(message, code); - } -} +export class ConnectedAccountRefreshAccessTokenException extends CustomException {} export enum ConnectedAccountRefreshAccessTokenExceptionCode { REFRESH_TOKEN_NOT_FOUND = 'REFRESH_TOKEN_NOT_FOUND', diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception.ts index 01bb60343f..578ab1804e 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception.ts @@ -1,10 +1,9 @@ import { CustomException } from 'src/utils/custom-exception'; +import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception'; -export class MessageImportDriverException extends CustomException { - constructor(message: string, code: MessageImportDriverExceptionCode) { - super(message, code); - } -} +export class MessageImportDriverException extends CustomException< + MessageImportDriverExceptionCode | MessageNetworkExceptionCode +> {} export enum MessageImportDriverExceptionCode { NOT_FOUND = 'NOT_FOUND', diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception.ts index 0c362604bc..1e0d6a965d 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception.ts @@ -1,10 +1,16 @@ import { CustomException } from 'src/utils/custom-exception'; -export class MicrosoftImportDriverException extends CustomException { +export class MicrosoftImportDriverException extends CustomException { statusCode: number; - constructor(message: string, code: string, statusCode: number) { - super(message, code); + constructor( + message: string, + code: string, + statusCode: number, + { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, + ) { + super(message, code, { + userFriendlyMessage: userFriendlyMessage ?? message, + }); this.statusCode = statusCode; - this.code = code; } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/exceptions/message-import.exception.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/exceptions/message-import.exception.ts index 6231c39bcf..31de71dbce 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/exceptions/message-import.exception.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/exceptions/message-import.exception.ts @@ -1,14 +1,8 @@ import { CustomException } from 'src/utils/custom-exception'; -export class MessageImportException extends CustomException { - constructor(message: string, code: MessageImportExceptionCode) { - super(message, code); - } -} +export class MessageImportException extends CustomException {} export enum MessageImportExceptionCode { UNKNOWN = 'UNKNOWN', PROVIDER_NOT_SUPPORTED = 'PROVIDER_NOT_SUPPORTED', - MESSAGE_CHANNEL_NOT_FOUND = 'MESSAGE_CHANNEL_NOT_FOUND', - FOLDER_ID_REQUIRED = 'FOLDER_ID_REQUIRED', } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts index 38cedd3e41..172630b73d 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts @@ -22,7 +22,6 @@ import { export enum MessageImportSyncStep { FULL_MESSAGE_LIST_FETCH = 'FULL_MESSAGE_LIST_FETCH', // TODO: deprecate to only use MESSAGE_LIST_FETCH - PARTIAL_MESSAGE_LIST_FETCH = 'PARTIAL_MESSAGE_LIST_FETCH', // TODO: deprecate to only use MESSAGE_LIST_FETCH MESSAGE_LIST_FETCH = 'MESSAGE_LIST_FETCH', MESSAGES_IMPORT_PENDING = 'MESSAGES_IMPORT_PENDING', MESSAGES_IMPORT_ONGOING = 'MESSAGES_IMPORT_ONGOING', diff --git a/packages/twenty-server/src/modules/view/views.exception.ts b/packages/twenty-server/src/modules/view/views.exception.ts index f71211ef4c..865b47bd00 100644 --- a/packages/twenty-server/src/modules/view/views.exception.ts +++ b/packages/twenty-server/src/modules/view/views.exception.ts @@ -1,21 +1,7 @@ import { CustomException } from 'src/utils/custom-exception'; -export class ViewException extends CustomException { - constructor(message: string, code: ViewExceptionCode) { - super(message, code); - } -} +export class ViewException extends CustomException {} export enum ViewExceptionCode { - VIEW_NOT_FOUND = 'VIEW_NOT_FOUND', - CANNOT_DELETE_INDEX_VIEW = 'CANNOT_DELETE_INDEX_VIEW', - METHOD_NOT_IMPLEMENTED = 'METHOD_NOT_IMPLEMENTED', CORE_VIEW_SYNC_ERROR = 'CORE_VIEW_SYNC_ERROR', } - -export enum ViewExceptionMessage { - VIEW_NOT_FOUND = 'View not found', - CANNOT_DELETE_INDEX_VIEW = 'Cannot delete index view', - METHOD_NOT_IMPLEMENTED = 'Method not implemented', - CORE_VIEW_SYNC_ERROR = 'Failed to sync view data to core', -} diff --git a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-common.exception.ts b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-common.exception.ts index 288c6ae591..5d718314b4 100644 --- a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-common.exception.ts +++ b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-common.exception.ts @@ -1,12 +1,7 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkflowCommonException extends CustomException { - constructor(message: string, code: WorkflowCommonExceptionCode) { - super(message, code); - } -} +export class WorkflowCommonException extends CustomException {} export enum WorkflowCommonExceptionCode { OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', - INVALID_CACHE_VERSION = 'INVALID_CACHE_VERSION', } diff --git a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-query-validation.exception.ts b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-query-validation.exception.ts index 4831630fd8..851a32b7f3 100644 --- a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-query-validation.exception.ts +++ b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-query-validation.exception.ts @@ -1,14 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkflowQueryValidationException extends CustomException { - constructor( - message: string, - code: WorkflowQueryValidationExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class WorkflowQueryValidationException extends CustomException {} export enum WorkflowQueryValidationExceptionCode { FORBIDDEN = 'FORBIDDEN', diff --git a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-version-step.exception.ts b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-version-step.exception.ts index 2082010c56..755a936705 100644 --- a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-version-step.exception.ts +++ b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-version-step.exception.ts @@ -1,14 +1,7 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkflowVersionStepException extends CustomException { - constructor( - message: string, - code: WorkflowVersionStepExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class WorkflowVersionStepException extends CustomException {} + export enum WorkflowVersionStepExceptionCode { UNKNOWN = 'UNKNOWN', NOT_FOUND = 'NOT_FOUND', diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception.ts deleted file mode 100644 index cdba717b8f..0000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CustomException } from 'src/utils/custom-exception'; - -export class WorkflowExecutorException extends CustomException { - constructor(message: string, code: string) { - super(message, code); - } -} - -export enum WorkflowExecutorExceptionCode { - WORKFLOW_FAILED = 'WORKFLOW_FAILED', -} diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts index 80b8550a09..d0d4038feb 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts @@ -1,20 +1,10 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkflowStepExecutorException extends CustomException { - constructor( - message: string, - code: WorkflowStepExecutorExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class WorkflowStepExecutorException extends CustomException {} export enum WorkflowStepExecutorExceptionCode { SCOPED_WORKSPACE_NOT_FOUND = 'SCOPED_WORKSPACE_NOT_FOUND', INVALID_STEP_TYPE = 'INVALID_STEP_TYPE', STEP_NOT_FOUND = 'STEP_NOT_FOUND', - INVALID_STEP_SETTINGS = 'INVALID_STEP_SETTINGS', INTERNAL_ERROR = 'INTERNAL_ERROR', - FAILED_TO_EXECUTE_STEP = 'FAILED_TO_EXECUTE_STEP', } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/exceptions/send-email-action.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/exceptions/send-email-action.exception.ts deleted file mode 100644 index 14722a1feb..0000000000 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/exceptions/send-email-action.exception.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CustomException } from 'src/utils/custom-exception'; - -export class SendEmailActionException extends CustomException { - constructor(message: string, code: SendEmailActionExceptionCode) { - super(message, code); - } -} - -export enum SendEmailActionExceptionCode { - PROVIDER_NOT_SUPPORTED = 'PROVIDER_NOT_SUPPORTED', - CONNECTED_ACCOUNT_NOT_FOUND = 'CONNECTED_ACCOUNT_NOT_FOUND', - INVALID_EMAIL = 'INVALID_EMAIL', - INVALID_CONNECTED_ACCOUNT_ID = 'INVALID_CONNECTED_ACCOUNT_ID', -} diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/exceptions/record-crud-action.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/exceptions/record-crud-action.exception.ts index d90380ec8b..4fecb06987 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/exceptions/record-crud-action.exception.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/exceptions/record-crud-action.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class RecordCRUDActionException extends CustomException { - constructor(message: string, code: RecordCRUDActionExceptionCode) { - super(message, code); - } -} +export class RecordCRUDActionException extends CustomException {} export enum RecordCRUDActionExceptionCode { INVALID_REQUEST = 'INVALID_REQUEST', diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/exceptions/workflow-run.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/exceptions/workflow-run.exception.ts index 37ddde5b4e..5b26bf661f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/exceptions/workflow-run.exception.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/exceptions/workflow-run.exception.ts @@ -1,10 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkflowRunException extends CustomException { - constructor(message: string, code: WorkflowRunExceptionCode) { - super(message, code); - } -} +export class WorkflowRunException extends CustomException {} export enum WorkflowRunExceptionCode { WORKFLOW_RUN_NOT_FOUND = 'WORKFLOW_RUN_NOT_FOUND', @@ -13,5 +9,4 @@ export enum WorkflowRunExceptionCode { INVALID_INPUT = 'INVALID_INPUT', WORKFLOW_RUN_LIMIT_REACHED = 'WORKFLOW_RUN_LIMIT_REACHED', WORKFLOW_RUN_INVALID = 'WORKFLOW_RUN_INVALID', - FAILURE = 'FAILURE', } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts index dd909983b5..c84059b27b 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts @@ -1,15 +1,6 @@ import { CustomException } from 'src/utils/custom-exception'; -export class WorkflowTriggerException extends CustomException { - declare code: WorkflowTriggerExceptionCode; - constructor( - message: string, - code: WorkflowTriggerExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, - ) { - super(message, code, userFriendlyMessage); - } -} +export class WorkflowTriggerException extends CustomException {} export enum WorkflowTriggerExceptionCode { INVALID_INPUT = 'INVALID_INPUT', diff --git a/packages/twenty-server/src/utils/__test__/custom-exception.spec.ts b/packages/twenty-server/src/utils/__test__/custom-exception.spec.ts new file mode 100644 index 0000000000..a746dd306b --- /dev/null +++ b/packages/twenty-server/src/utils/__test__/custom-exception.spec.ts @@ -0,0 +1,93 @@ +import { + appendCommonExceptionCode, + CustomException, + UnknownException, +} from 'src/utils/custom-exception'; + +describe('appendCommonExceptionCode', () => { + it('should merge CommonExceptionCode with specific exception code', () => { + const specificExceptionCode = { + SPECIFIC_ERROR: 'SPECIFIC_ERROR', + }; + + const result = appendCommonExceptionCode(specificExceptionCode); + + expect(result).toEqual({ + INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR', + SPECIFIC_ERROR: 'SPECIFIC_ERROR', + }); + }); + + it('should return CommonExceptionCode when empty object is provided', () => { + const result = appendCommonExceptionCode({}); + + expect(result).toEqual({ + INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR', + }); + }); +}); + +describe('CustomException', () => { + // Create a concrete implementation of the abstract class for testing + class TestException extends CustomException {} + + it('should set message and code correctly', () => { + const message = 'Test error message'; + const code = 'TEST_ERROR'; + const exception = new TestException(message, code); + + expect(exception.message).toBe(message); + expect(exception.code).toBe(code); + expect(exception.userFriendlyMessage).toBeUndefined(); + }); + + it('should set userFriendlyMessage when provided', () => { + const message = 'Test error message'; + const code = 'TEST_ERROR'; + const userFriendlyMessage = 'User friendly error message'; + const exception = new TestException(message, code, { + userFriendlyMessage, + }); + + expect(exception.message).toBe(message); + expect(exception.code).toBe(code); + expect(exception.userFriendlyMessage).toBe(userFriendlyMessage); + }); + + it('should extend Error', () => { + const exception = new TestException('Test error', 'TEST_ERROR'); + + expect(exception).toBeInstanceOf(Error); + }); +}); + +describe('UnknownException', () => { + it('should extend CustomException', () => { + const exception = new UnknownException('Test error', 'TEST_ERROR'); + + expect(exception).toBeInstanceOf(CustomException); + }); + + it('should set message and code correctly', () => { + const message = 'Test error message'; + const code = 'TEST_ERROR'; + const exception = new UnknownException(message, code); + + expect(exception.message).toBe(message); + expect(exception.code).toBe(code); + expect(exception.userFriendlyMessage).toBeUndefined(); + }); + + it('should set userFriendlyMessage when provided', () => { + const message = 'Test error message'; + const code = 'TEST_ERROR'; + const userFriendlyMessage = 'User friendly error message'; + const exception = new UnknownException(message, code, { + userFriendlyMessage, + }); + + expect(exception.message).toBe(message); + expect(exception.code).toBe(code); + expect(exception.userFriendlyMessage).toBe(userFriendlyMessage); + }); +}); diff --git a/packages/twenty-server/src/utils/custom-exception.ts b/packages/twenty-server/src/utils/custom-exception.ts index a6f6c88cd8..24bda4faab 100644 --- a/packages/twenty-server/src/utils/custom-exception.ts +++ b/packages/twenty-server/src/utils/custom-exception.ts @@ -1,10 +1,44 @@ -export class CustomException extends Error { - code: string; - userFriendlyMessage?: string; +const CommonExceptionCode = { + INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR', +} as const; - constructor(message: string, code: string, userFriendlyMessage?: string) { +export const appendCommonExceptionCode = < + SpecificExceptionCode = Record, +>( + specificExceptionCode: SpecificExceptionCode, +) => { + return { + ...CommonExceptionCode, + ...specificExceptionCode, + } as const; +}; + +export abstract class CustomException< + ExceptionCode extends string = string, + ForceFriendlyMessage = false, + ExceptionMessage extends string = string, + ExceptionFriendlyMessage extends string = string, +> extends Error { + code: ExceptionCode; + userFriendlyMessage?: ExceptionFriendlyMessage; + + constructor( + message: ExceptionMessage, + code: ExceptionCode, + ...userFriendlyMessage: ForceFriendlyMessage extends true + ? [{ userFriendlyMessage: ExceptionFriendlyMessage }] + : [{ userFriendlyMessage?: ExceptionFriendlyMessage }?] + ) { super(message); this.code = code; - this.userFriendlyMessage = userFriendlyMessage; + this.userFriendlyMessage = userFriendlyMessage + ? userFriendlyMessage?.[0]?.userFriendlyMessage + : undefined; } } + +/** + * Exception class for test scenarios and edge cases. + * Prefer domain-specific exceptions in production code. + */ +export class UnknownException extends CustomException {} diff --git a/yarn.lock b/yarn.lock index f44fdbb1fb..c97b24a8fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30759,9 +30759,9 @@ __metadata: languageName: node linkType: hard -"cloudflare@npm:^4.0.0": - version: 4.0.0 - resolution: "cloudflare@npm:4.0.0" +"cloudflare@npm:^4.5.0": + version: 4.5.0 + resolution: "cloudflare@npm:4.5.0" dependencies: "@types/node": "npm:^18.11.18" "@types/node-fetch": "npm:^2.6.4" @@ -30770,7 +30770,7 @@ __metadata: form-data-encoder: "npm:1.7.2" formdata-node: "npm:^4.3.2" node-fetch: "npm:^2.6.7" - checksum: 10c0/d83a4d0544de5794935acb802c875c6f718713d8c7613b2a74d6145b922f18415e514cc57bbef9249726d83c0788ac8a72517a69efb1fd5b5af58b654403c375 + checksum: 10c0/376ca8cde08878e383a151626c40f702160565776cdb455f6091009b6733628bae266bf47d61c8e2ff21fb94388f0a609af24872dea7c01a1b3e26d76deec8db languageName: node linkType: hard @@ -57194,7 +57194,7 @@ __metadata: cache-manager-redis-yet: "npm:^4.1.2" class-validator: "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch" class-validator-jsonschema: "npm:^5.0.2" - cloudflare: "npm:^4.0.0" + cloudflare: "npm:^4.5.0" connect-redis: "npm:^7.1.1" express-session: "npm:^1.18.1" graphql-middleware: "npm:^6.1.35"