mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
feat(domain-manager): refactor custom domain validation and improve c… (#13388)
This commit is contained in:
parent
51340f2b0e
commit
23353e31e6
87 changed files with 779 additions and 901 deletions
|
|
@ -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 = () => {
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
<StyledButtonGroup>
|
||||
<StyledButton
|
||||
isLoading={isLoading}
|
||||
Icon={IconReload}
|
||||
title={t`Reload`}
|
||||
variant="primary"
|
||||
onClick={checkCustomDomainRecords}
|
||||
type="button"
|
||||
/>
|
||||
<StyledButton
|
||||
Icon={IconTrash}
|
||||
variant="primary"
|
||||
onClick={deleteCustomDomain}
|
||||
/>
|
||||
</StyledButtonGroup>
|
||||
{currentWorkspace?.customDomain && (
|
||||
<StyledButtonGroup>
|
||||
<StyledButton
|
||||
isLoading={isLoading}
|
||||
Icon={IconReload}
|
||||
title={t`Reload`}
|
||||
variant="primary"
|
||||
onClick={checkCustomDomainRecords}
|
||||
type="button"
|
||||
/>
|
||||
<StyledButton
|
||||
Icon={IconTrash}
|
||||
variant="primary"
|
||||
onClick={deleteCustomDomain}
|
||||
/>
|
||||
</StyledButtonGroup>
|
||||
)}
|
||||
</StyledDomainFormWrapper>
|
||||
{currentWorkspace?.customDomain && (
|
||||
<StyledRecordsWrapper>
|
||||
<SettingsCustomDomainRecordsStatus />
|
||||
{customDomainRecords &&
|
||||
customDomainRecords.records.some(
|
||||
(record) => record.status !== 'success',
|
||||
) && (
|
||||
<SettingsCustomDomainRecords
|
||||
records={customDomainRecords.records}
|
||||
/>
|
||||
)}
|
||||
{customDomainRecords && (
|
||||
<SettingsCustomDomainRecords
|
||||
records={customDomainRecords.records}
|
||||
/>
|
||||
)}
|
||||
</StyledRecordsWrapper>
|
||||
)}
|
||||
</Section>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<StyledTable>
|
||||
<TableRow gridAutoColumns="35% 16% auto">
|
||||
<TableRow gridAutoColumns="30% 16% 38% 16%">
|
||||
<TableHeader>Name</TableHeader>
|
||||
<TableHeader>Type</TableHeader>
|
||||
<TableHeader>Value</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
{records
|
||||
.filter((record) => record.status !== 'success')
|
||||
.map((record) => (
|
||||
<TableRow gridAutoColumns="30% 16% auto" key={record.key}>
|
||||
<StyledTableCell>
|
||||
<StyledButton
|
||||
title={record.key}
|
||||
onClick={() => copyToClipboardDebounced(record.key)}
|
||||
type="button"
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<StyledButton
|
||||
title={record.type.toUpperCase()}
|
||||
onClick={() =>
|
||||
copyToClipboardDebounced(record.type.toUpperCase())
|
||||
}
|
||||
type="button"
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<StyledButton
|
||||
title={record.value}
|
||||
onClick={() => copyToClipboardDebounced(record.value)}
|
||||
type="button"
|
||||
/>
|
||||
</StyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{rows.map((row) => (
|
||||
<TableRow gridAutoColumns="30% 16% 38% 16%" key={row.name}>
|
||||
<StyledTableCell>
|
||||
<StyledButton
|
||||
title={row.key}
|
||||
onClick={() => copyToClipboardDebounced(row.key)}
|
||||
type="button"
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<StyledButton
|
||||
title={row.type.toUpperCase()}
|
||||
onClick={() => copyToClipboardDebounced(row.type.toUpperCase())}
|
||||
type="button"
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<StyledButton
|
||||
title={row.value}
|
||||
onClick={() => copyToClipboardDebounced(row.value)}
|
||||
type="button"
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Status color={row.color} text={capitalize(row.status)} />
|
||||
</StyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</StyledTable>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<StyledTable>
|
||||
{rows.map((row) => (
|
||||
<StyledTableRow key={row.name}>
|
||||
<StyledTableCell>{row.name}</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Status color={row.color} text={capitalize(row.status)} />
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</StyledTable>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,
|
||||
}));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<GraphqlQueryRunnerExceptionCode> {}
|
||||
|
||||
export enum GraphqlQueryRunnerExceptionCode {
|
||||
INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<ApiKeyExceptionCode> {}
|
||||
|
||||
export enum ApiKeyExceptionCode {
|
||||
API_KEY_NOT_FOUND = 'API_KEY_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<ApprovedAccessDomainExceptionCode> {}
|
||||
|
||||
export enum ApprovedAccessDomainExceptionCode {
|
||||
APPROVED_ACCESS_DOMAIN_NOT_FOUND = 'APPROVED_ACCESS_DOMAIN_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<AuditExceptionCode> {}
|
||||
|
||||
export enum AuditExceptionCode {
|
||||
INVALID_TYPE = 'INVALID_TYPE',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<BillingExceptionCode> {}
|
||||
|
||||
export enum BillingExceptionCode {
|
||||
BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<CaptchaExceptionCode> {}
|
||||
|
||||
export enum CaptchaExceptionCode {
|
||||
INVALID_CAPTCHA = 'INVALID_CAPTCHA',
|
||||
|
|
|
|||
|
|
@ -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<Workspace> = {
|
||||
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<Workspace> = {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
await this.messageQueueService.addCron<undefined>({
|
||||
jobName: CheckCustomDomainValidRecordsCronJob.name,
|
||||
data: undefined,
|
||||
options: {
|
||||
repeat: { pattern: CHECK_CUSTOM_DOMAIN_VALID_RECORDS_CRON_PATTERN },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Workspace>,
|
||||
private readonly customDomainService: CustomDomainService,
|
||||
) {}
|
||||
|
||||
@Process(CheckCustomDomainValidRecordsCronJob.name)
|
||||
@SentryCronMonitor(
|
||||
CheckCustomDomainValidRecordsCronJob.name,
|
||||
CHECK_CUSTOM_DOMAIN_VALID_RECORDS_CRON_PATTERN,
|
||||
)
|
||||
async handle(): Promise<void> {
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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<CustomDomainValidRecords | undefined> {
|
||||
return this.customDomainService.checkCustomDomainValidRecords(workspace);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<Workspace>,
|
||||
) {
|
||||
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<CustomDomainValidRecords['records'][0] | undefined>((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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<EmailVerificationExceptionCode> {}
|
||||
|
||||
export enum EmailVerificationExceptionCode {
|
||||
EMAIL_VERIFICATION_NOT_REQUIRED = 'EMAIL_VERIFICATION_NOT_REQUIRED',
|
||||
|
|
|
|||
|
|
@ -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<FeatureFlagExceptionCode> {}
|
||||
|
||||
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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<FileEntity>,
|
||||
private readonly fileMetadataService: FileMetadataService,
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
) {}
|
||||
|
||||
@Process(CleanupOrphanedFilesCronJob.name)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<RecordTransformerExceptionCode> {}
|
||||
|
||||
export enum RecordTransformerExceptionCode {
|
||||
INVALID_URL = 'INVALID_URL',
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<SearchExceptionCode> {}
|
||||
|
||||
export enum SearchExceptionCode {
|
||||
LABEL_IDENTIFIER_FIELD_NOT_FOUND = 'LABEL_IDENTIFIER_FIELD_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<SSOExceptionCode> {}
|
||||
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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<ThrottlerExceptionCode> {}
|
||||
|
||||
export enum ThrottlerExceptionCode {
|
||||
LIMIT_REACHED = 'LIMIT_REACHED',
|
||||
|
|
|
|||
|
|
@ -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<SendEmailToolExceptionCode> {}
|
||||
|
||||
export enum SendEmailToolExceptionCode {
|
||||
INVALID_CONNECTED_ACCOUNT_ID = 'INVALID_CONNECTED_ACCOUNT_ID',
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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<ConfigVariableExceptionCode> {}
|
||||
|
||||
export enum ConfigVariableExceptionCode {
|
||||
DATABASE_CONFIG_DISABLED = 'DATABASE_CONFIG_DISABLED',
|
||||
|
|
|
|||
|
|
@ -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<TwoFactorAuthenticationExceptionCode> {}
|
||||
|
||||
export enum TwoFactorAuthenticationExceptionCode {
|
||||
INVALID_CONFIGURATION = 'INVALID_CONFIGURATION',
|
||||
|
|
|
|||
|
|
@ -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<UserWorkspaceExceptionCode> {}
|
||||
|
||||
export enum UserWorkspaceExceptionCode {
|
||||
USER_WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<UserExceptionCode> {}
|
||||
|
||||
export enum UserExceptionCode {
|
||||
USER_NOT_FOUND = 'USER_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<WebhookExceptionCode> {}
|
||||
|
||||
export enum WebhookExceptionCode {
|
||||
WEBHOOK_NOT_FOUND = 'WEBHOOK_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<WorkspaceInvitationExceptionCode> {}
|
||||
|
||||
export enum WorkspaceInvitationExceptionCode {
|
||||
INVALID_APP_TOKEN_TYPE = 'INVALID_APP_TOKEN_TYPE',
|
||||
|
|
|
|||
|
|
@ -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<Workspace> {
|
|||
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<Workspace> {
|
|||
await this.customDomainService.deleteCustomHostnameByHostnameSilently(
|
||||
workspace.customDomain,
|
||||
);
|
||||
workspace.isCustomDomainEnabled = false;
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
@ -401,38 +396,6 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<WorkspaceExceptionCode> {}
|
||||
|
||||
export enum WorkspaceExceptionCode {
|
||||
SUBDOMAIN_NOT_FOUND = 'SUBDOMAIN_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<CustomDomainValidRecords | undefined> {
|
||||
return this.workspaceService.checkCustomDomainValidRecords(workspace);
|
||||
}
|
||||
|
||||
@Query(() => PublicWorkspaceDataOutput)
|
||||
@UseGuards(PublicEndpointGuard)
|
||||
async getPublicWorkspaceDataByDomain(
|
||||
|
|
|
|||
|
|
@ -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<AgentExceptionCode> {}
|
||||
|
||||
export enum AgentExceptionCode {
|
||||
AGENT_NOT_FOUND = 'AGENT_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<DataSourceExceptionCode> {}
|
||||
|
||||
export enum DataSourceExceptionCode {
|
||||
DATA_SOURCE_NOT_FOUND = 'DATA_SOURCE_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<ObjectMetadataExceptionCode> {}
|
||||
|
||||
export enum ObjectMetadataExceptionCode {
|
||||
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<PermissionsExceptionCode> {}
|
||||
|
||||
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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<RemoteServerExceptionCode> {}
|
||||
|
||||
export enum RemoteServerExceptionCode {
|
||||
REMOTE_SERVER_NOT_FOUND = 'REMOTE_SERVER_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<ForeignTableExceptionCode> {}
|
||||
|
||||
export enum ForeignTableExceptionCode {
|
||||
FOREIGN_TABLE_MUTATION_NOT_ALLOWED = 'FOREIGN_TABLE_MUTATION_NOT_ALLOWED',
|
||||
|
|
|
|||
|
|
@ -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<RemoteTableExceptionCode> {}
|
||||
|
||||
export enum RemoteTableExceptionCode {
|
||||
REMOTE_TABLE_NOT_FOUND = 'REMOTE_TABLE_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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.` },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ServerlessFunctionExceptionCode> {}
|
||||
|
||||
export enum ServerlessFunctionExceptionCode {
|
||||
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<InvalidMetadataExceptionCode> {}
|
||||
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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<WorkspaceMetadataCacheExceptionCode> {}
|
||||
|
||||
export enum WorkspaceMetadataCacheExceptionCode {
|
||||
OBJECT_METADATA_MAP_NOT_FOUND = 'Object Metadata map not found',
|
||||
|
|
|
|||
|
|
@ -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<WorkspaceMetadataVersionExceptionCode> {}
|
||||
|
||||
export enum WorkspaceMetadataVersionExceptionCode {
|
||||
METADATA_VERSION_NOT_FOUND = 'METADATA_VERSION_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<WorkspaceMigrationExceptionCode> {}
|
||||
|
||||
export enum WorkspaceMigrationExceptionCode {
|
||||
NO_FACTORY_FOUND = 'NO_FACTORY_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<RelationExceptionCode> {}
|
||||
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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<TwentyORMExceptionCode> {}
|
||||
|
||||
export enum TwentyORMExceptionCode {
|
||||
METADATA_VERSION_MISMATCH = 'METADATA_VERSION_MISMATCH',
|
||||
|
|
|
|||
|
|
@ -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<WorkspaceCleanerExceptionCode> {}
|
||||
|
||||
export enum WorkspaceCleanerExceptionCode {
|
||||
BILLING_SUBSCRIPTION_NOT_FOUND = 'BILLING_SUBSCRIPTION_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<CalendarEventImportDriverExceptionCode> {}
|
||||
|
||||
export enum CalendarEventImportDriverExceptionCode {
|
||||
NOT_FOUND = 'NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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<CalendarEventImportExceptionCode> {}
|
||||
|
||||
export enum CalendarEventImportExceptionCode {
|
||||
PROVIDER_NOT_SUPPORTED = 'PROVIDER_NOT_SUPPORTED',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
CALENDAR_CHANNEL_NOT_FOUND = 'CALENDAR_CHANNEL_NOT_FOUND',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ConnectedAccountRefreshAccessTokenExceptionCode> {}
|
||||
|
||||
export enum ConnectedAccountRefreshAccessTokenExceptionCode {
|
||||
REFRESH_TOKEN_NOT_FOUND = 'REFRESH_TOKEN_NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class MicrosoftImportDriverException extends CustomException {
|
||||
export class MicrosoftImportDriverException extends CustomException<string> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MessageImportExceptionCode> {}
|
||||
|
||||
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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<ViewExceptionCode> {}
|
||||
|
||||
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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WorkflowCommonExceptionCode> {}
|
||||
|
||||
export enum WorkflowCommonExceptionCode {
|
||||
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
|
||||
INVALID_CACHE_VERSION = 'INVALID_CACHE_VERSION',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WorkflowQueryValidationExceptionCode> {}
|
||||
|
||||
export enum WorkflowQueryValidationExceptionCode {
|
||||
FORBIDDEN = 'FORBIDDEN',
|
||||
|
|
|
|||
|
|
@ -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<WorkflowVersionStepExceptionCode> {}
|
||||
|
||||
export enum WorkflowVersionStepExceptionCode {
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
NOT_FOUND = 'NOT_FOUND',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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<WorkflowStepExecutorExceptionCode> {}
|
||||
|
||||
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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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<RecordCRUDActionExceptionCode> {}
|
||||
|
||||
export enum RecordCRUDActionExceptionCode {
|
||||
INVALID_REQUEST = 'INVALID_REQUEST',
|
||||
|
|
|
|||
|
|
@ -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<WorkflowRunExceptionCode> {}
|
||||
|
||||
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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WorkflowTriggerExceptionCode> {}
|
||||
|
||||
export enum WorkflowTriggerExceptionCode {
|
||||
INVALID_INPUT = 'INVALID_INPUT',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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<string, string>,
|
||||
>(
|
||||
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 {}
|
||||
|
|
|
|||
10
yarn.lock
10
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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue