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"