diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 940cd907f..972d49a19 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -389,21 +389,15 @@ export type CustomHostnameDetails = { __typename?: 'CustomHostnameDetails'; hostname: Scalars['String']['output']; id: Scalars['String']['output']; - ownershipVerifications: Array; - status?: Maybe; + records: Array; }; -export type CustomHostnameOwnershipVerificationHttp = { - __typename?: 'CustomHostnameOwnershipVerificationHttp'; - body: Scalars['String']['output']; - type: Scalars['String']['output']; - url: Scalars['String']['output']; -}; - -export type CustomHostnameOwnershipVerificationTxt = { - __typename?: 'CustomHostnameOwnershipVerificationTxt'; - name: Scalars['String']['output']; +export type CustomHostnameVerification = { + __typename?: 'CustomHostnameVerification'; + key: Scalars['String']['output']; + status: Scalars['String']['output']; type: Scalars['String']['output']; + validationType: Scalars['String']['output']; value: Scalars['String']['output']; }; @@ -1313,8 +1307,6 @@ export type OnboardingStepSuccess = { success: Scalars['Boolean']['output']; }; -export type OwnershipVerification = CustomHostnameOwnershipVerificationHttp | CustomHostnameOwnershipVerificationTxt; - export type PageInfo = { __typename?: 'PageInfo'; /** The cursor of the last returned record. */ @@ -1755,6 +1747,16 @@ export enum ServerlessFunctionSyncStatus { READY = 'READY' } +export enum SettingsFeatures { + ADMIN_PANEL = 'ADMIN_PANEL', + API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS', + DATA_MODEL = 'DATA_MODEL', + ROLES = 'ROLES', + SECURITY_SETTINGS = 'SECURITY_SETTINGS', + WORKSPACE_SETTINGS = 'WORKSPACE_SETTINGS', + WORKSPACE_USERS = 'WORKSPACE_USERS' +} + export type SetupOidcSsoInput = { clientID: Scalars['String']['input']; clientSecret: Scalars['String']['input']; @@ -1985,7 +1987,8 @@ export type User = { analyticsTinybirdJwts?: Maybe; canImpersonate: Scalars['Boolean']['output']; createdAt: Scalars['DateTime']['output']; - currentWorkspace: Workspace; + currentUserWorkspace?: Maybe; + currentWorkspace?: Maybe; defaultAvatarUrl?: Maybe; deletedAt?: Maybe; disabled?: Maybe; @@ -2060,6 +2063,7 @@ export type UserWorkspace = { createdAt: Scalars['DateTime']['output']; deletedAt?: Maybe; id: Scalars['UUID']['output']; + settingsPermissions?: Maybe>; updatedAt: Scalars['DateTime']['output']; user: User; userId: Scalars['String']['output']; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index d7620c7e4..d3a7b7ae5 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -326,21 +326,15 @@ export type CustomHostnameDetails = { __typename?: 'CustomHostnameDetails'; hostname: Scalars['String']; id: Scalars['String']; - ownershipVerifications: Array; - status?: Maybe; + records: Array; }; -export type CustomHostnameOwnershipVerificationHttp = { - __typename?: 'CustomHostnameOwnershipVerificationHttp'; - body: Scalars['String']; - type: Scalars['String']; - url: Scalars['String']; -}; - -export type CustomHostnameOwnershipVerificationTxt = { - __typename?: 'CustomHostnameOwnershipVerificationTxt'; - name: Scalars['String']; +export type CustomHostnameVerification = { + __typename?: 'CustomHostnameVerification'; + key: Scalars['String']; + status: Scalars['String']; type: Scalars['String']; + validationType: Scalars['String']; value: Scalars['String']; }; @@ -1181,8 +1175,6 @@ export type OnboardingStepSuccess = { success: Scalars['Boolean']; }; -export type OwnershipVerification = CustomHostnameOwnershipVerificationHttp | CustomHostnameOwnershipVerificationTxt; - export type PageInfo = { __typename?: 'PageInfo'; /** The cursor of the last returned record. */ @@ -1246,8 +1238,8 @@ export type Query = { findWorkspaceFromInviteHash: Workspace; findWorkspaceInvitations: Array; getAvailablePackages: Scalars['JSON']; + getCustomHostnameDetails?: Maybe; getEnvironmentVariablesGrouped: EnvironmentVariablesOutput; - getHostnameDetails?: Maybe; getPostgresCredentials?: Maybe; getProductPrices: BillingProductPricesOutput; getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput; @@ -2432,10 +2424,10 @@ export type UploadWorkspaceLogoMutationVariables = Exact<{ export type UploadWorkspaceLogoMutation = { __typename?: 'Mutation', uploadWorkspaceLogo: string }; -export type GetHostnameDetailsQueryVariables = Exact<{ [key: string]: never; }>; +export type GetCustomHostnameDetailsQueryVariables = Exact<{ [key: string]: never; }>; -export type GetHostnameDetailsQuery = { __typename?: 'Query', getHostnameDetails?: { __typename?: 'CustomHostnameDetails', hostname: string, status?: string | null, ownershipVerifications: Array<{ __typename?: 'CustomHostnameOwnershipVerificationHttp', type: string, body: string, url: string } | { __typename?: 'CustomHostnameOwnershipVerificationTxt', type: string, name: string, value: string }> } | null }; +export type GetCustomHostnameDetailsQuery = { __typename?: 'Query', getCustomHostnameDetails?: { __typename?: 'CustomHostnameDetails', hostname: string, records: Array<{ __typename?: 'CustomHostnameVerification', type: string, key: string, value: string, validationType: string, status: string }> } | null }; export type GetWorkspaceFromInviteHashQueryVariables = Exact<{ inviteHash: Scalars['String']; @@ -4868,53 +4860,47 @@ export function useUploadWorkspaceLogoMutation(baseOptions?: Apollo.MutationHook export type UploadWorkspaceLogoMutationHookResult = ReturnType; export type UploadWorkspaceLogoMutationResult = Apollo.MutationResult; export type UploadWorkspaceLogoMutationOptions = Apollo.BaseMutationOptions; -export const GetHostnameDetailsDocument = gql` - query GetHostnameDetails { - getHostnameDetails { +export const GetCustomHostnameDetailsDocument = gql` + query GetCustomHostnameDetails { + getCustomHostnameDetails { hostname - ownershipVerifications { - ... on CustomHostnameOwnershipVerificationTxt { - type - name - value - } - ... on CustomHostnameOwnershipVerificationHttp { - type - body - url - } + records { + type + key + value + validationType + status } - status } } `; /** - * __useGetHostnameDetailsQuery__ + * __useGetCustomHostnameDetailsQuery__ * - * To run a query within a React component, call `useGetHostnameDetailsQuery` and pass it any options that fit your needs. - * When your component renders, `useGetHostnameDetailsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useGetCustomHostnameDetailsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetCustomHostnameDetailsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useGetHostnameDetailsQuery({ + * const { data, loading, error } = useGetCustomHostnameDetailsQuery({ * variables: { * }, * }); */ -export function useGetHostnameDetailsQuery(baseOptions?: Apollo.QueryHookOptions) { +export function useGetCustomHostnameDetailsQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetHostnameDetailsDocument, options); + return Apollo.useQuery(GetCustomHostnameDetailsDocument, options); } -export function useGetHostnameDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useGetCustomHostnameDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetHostnameDetailsDocument, options); + return Apollo.useLazyQuery(GetCustomHostnameDetailsDocument, options); } -export type GetHostnameDetailsQueryHookResult = ReturnType; -export type GetHostnameDetailsLazyQueryHookResult = ReturnType; -export type GetHostnameDetailsQueryResult = Apollo.QueryResult; +export type GetCustomHostnameDetailsQueryHookResult = ReturnType; +export type GetCustomHostnameDetailsLazyQueryHookResult = ReturnType; +export type GetCustomHostnameDetailsQueryResult = Apollo.QueryResult; export const GetWorkspaceFromInviteHashDocument = gql` query GetWorkspaceFromInviteHash($inviteHash: String!) { findWorkspaceFromInviteHash(inviteHash: $inviteHash) { diff --git a/packages/twenty-front/src/modules/workspace/graphql/queries/getHostnameDetails.ts b/packages/twenty-front/src/modules/workspace/graphql/queries/getHostnameDetails.ts index 113318e45..ae99d2eff 100644 --- a/packages/twenty-front/src/modules/workspace/graphql/queries/getHostnameDetails.ts +++ b/packages/twenty-front/src/modules/workspace/graphql/queries/getHostnameDetails.ts @@ -1,22 +1,16 @@ import { gql } from '@apollo/client'; -export const GET_HOSTNAME_DETAILS = gql` - query GetHostnameDetails { - getHostnameDetails { +export const GET_CUSTOM_HOSTNAME_DETAILS = gql` + query GetCustomHostnameDetails { + getCustomHostnameDetails { hostname - ownershipVerifications { - ... on CustomHostnameOwnershipVerificationTxt { - type - name - value - } - ... on CustomHostnameOwnershipVerificationHttp { - type - body - url - } + records { + type + key + value + validationType + status } - status } } `; diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx index f702e2b37..7138bf4b6 100644 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx @@ -1,5 +1,8 @@ import { ApolloError } from '@apollo/client'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { + CurrentWorkspace, + currentWorkspaceState, +} from '@/auth/states/currentWorkspaceState'; import { useRecoilState } from 'recoil'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; @@ -22,6 +25,7 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsHostnameEffect } from '~/pages/settings/workspace/SettingsHostnameEffect'; +import { isDefined } from 'twenty-shared'; export const SettingsDomain = () => { const navigate = useNavigateSettings(); @@ -36,6 +40,17 @@ export const SettingsDomain = () => { .regex(/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/, { message: t`Use letter, number and dash only. Start and finish with a letter or a number`, }), + hostname: z + .string() + .regex( + /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/, + { + message: t`Invalid custom hostname. Custom hostnames have to be smaller than 256 characters in length, cannot be IP addresses, cannot contain spaces, cannot contain any special characters such as _~\`!@#$%^*()=+{}[]|\\;:'",<>/? and cannot begin or end with a '-' character.`, + }, + ) + .max(256) + .optional() + .or(z.literal('')), }) .required(); @@ -53,30 +68,62 @@ export const SettingsDomain = () => { const form = useForm<{ subdomain: string; + hostname: string | null; }>({ mode: 'onChange', delayError: 500, defaultValues: { subdomain: currentWorkspace?.subdomain ?? '', + hostname: currentWorkspace?.hostname ?? null, }, resolver: zodResolver(validationSchema), }); const subdomainValue = form.watch('subdomain'); + const hostnameValue = form.watch('hostname'); - const handleSave = async () => { - const values = form.getValues(); - - if (!values || !form.formState.isValid || !currentWorkspace) { - return enqueueSnackBar(t`Invalid form values`, { - variant: SnackBarVariant.Error, - }); - } - - await updateWorkspace({ + const updateHostname = ( + hostname: string | null | undefined, + currentWorkspace: CurrentWorkspace, + ) => { + updateWorkspace({ variables: { input: { - subdomain: values.subdomain, + hostname: + isDefined(hostname) && hostname.length > 0 ? hostname : null, + }, + }, + onCompleted: () => { + setCurrentWorkspace({ + ...currentWorkspace, + hostname: hostname, + }); + }, + onError: (error) => { + if ( + error instanceof ApolloError && + error.graphQLErrors[0]?.extensions?.code === 'CONFLICT' + ) { + return form.control.setError('subdomain', { + type: 'manual', + message: t`Subdomain already taken`, + }); + } + enqueueSnackBar((error as Error).message, { + variant: SnackBarVariant.Error, + }); + }, + }); + }; + + const updateSubdomain = ( + subdomain: string, + currentWorkspace: CurrentWorkspace, + ) => { + updateWorkspace({ + variables: { + input: { + subdomain, }, }, onError: (error) => { @@ -98,11 +145,11 @@ export const SettingsDomain = () => { currentUrl.hostname = new URL( currentWorkspace.workspaceUrls.subdomainUrl, - ).hostname.replace(currentWorkspace.subdomain, values.subdomain); + ).hostname.replace(currentWorkspace.subdomain, subdomain); setCurrentWorkspace({ ...currentWorkspace, - subdomain: values.subdomain, + subdomain, }); redirectToWorkspaceDomain(currentUrl.toString()); @@ -110,6 +157,27 @@ export const SettingsDomain = () => { }); }; + const handleSave = async () => { + const values = form.getValues(); + + if (!values || !form.formState.isValid || !currentWorkspace) { + return enqueueSnackBar(t`Invalid form values`, { + variant: SnackBarVariant.Error, + }); + } + + if ( + isDefined(values.subdomain) && + values.subdomain !== currentWorkspace.subdomain + ) { + return updateSubdomain(values.subdomain, currentWorkspace); + } + + if (values.hostname !== currentWorkspace.hostname) { + return updateHostname(values.hostname, currentWorkspace); + } + }; + return ( { navigate(SettingsPath.Workspace)} onSave={handleSave} @@ -138,15 +207,15 @@ export const SettingsDomain = () => { {/* eslint-disable-next-line react/jsx-props-no-spreading */} + {(!currentWorkspace?.hostname || !isCustomDomainEnabled) && ( + + )} {isCustomDomainEnabled && ( <> )} - {(!currentWorkspace?.hostname || !isCustomDomainEnabled) && ( - - )} diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsHostname.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsHostname.tsx index d01d412db..6a7584433 100644 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsHostname.tsx +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsHostname.tsx @@ -1,35 +1,10 @@ -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import styled from '@emotion/styled'; -import { zodResolver } from '@hookform/resolvers/zod'; import { useLingui } from '@lingui/react/macro'; -import { Controller, useForm } from 'react-hook-form'; -import { useRecoilState } from 'recoil'; -import { isDefined } from 'twenty-shared'; -import { Button, H2Title, Section } from 'twenty-ui'; -import { z } from 'zod'; -import { - useGetHostnameDetailsQuery, - useUpdateWorkspaceMutation, -} from '~/generated/graphql'; - -const validationSchema = z - .object({ - hostname: z - .string() - .regex( - /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/, - { - message: - "Invalid custom hostname. Custom hostnames have to be smaller than 256 characters in length, cannot be IP addresses, cannot contain spaces, cannot contain any special characters such as _~`!@#$%^*()=+{}[]|\\;:'\",<>/? and cannot begin or end with a '-' character.", - }, - ) - .max(256) - .nullable(), - }) - .required(); - -type Form = z.infer; +import { Controller, useFormContext } from 'react-hook-form'; +import { H2Title, Section } from 'twenty-ui'; +import { useGetCustomHostnameDetailsQuery } from '~/generated/graphql'; +import { SettingsHostnameRecords } from '~/pages/settings/workspace/SettingsHostnameRecords'; const StyledDomainFromWrapper = styled.div` align-items: center; @@ -37,78 +12,13 @@ const StyledDomainFromWrapper = styled.div` `; export const SettingsHostname = () => { - const [updateWorkspace] = useUpdateWorkspaceMutation(); - const { data: getHostnameDetailsData } = useGetHostnameDetailsQuery(); + const { data: getHostnameDetailsData } = useGetCustomHostnameDetailsQuery(); const { t } = useLingui(); - const [currentWorkspace, setCurrentWorkspace] = useRecoilState( - currentWorkspaceState, - ); - - const { - control, - getValues, - clearErrors, - handleSubmit, - formState: { isValid }, - } = useForm
({ - mode: 'onSubmit', - defaultValues: { - hostname: currentWorkspace?.hostname ?? '', - }, - resolver: zodResolver(validationSchema), - }); - - const handleDelete = async () => { - try { - if (!currentWorkspace) { - throw new Error('Invalid form values'); - } - - await updateWorkspace({ - variables: { - input: { - hostname: null, - }, - }, - }); - } catch (error) { - control.setError('hostname', { - type: 'manual', - message: (error as Error).message, - }); - } - }; - - const handleSave = async () => { - const values = getValues(); - try { - clearErrors(); - - if (!values || !isValid || !currentWorkspace) { - throw new Error('Invalid form values'); - } - - await updateWorkspace({ - variables: { - input: { - hostname: values.hostname, - }, - }, - }); - - setCurrentWorkspace({ - ...currentWorkspace, - hostname: values.hostname, - }); - } catch (error) { - control.setError('hostname', { - type: 'manual', - message: (error as Error).message, - }); - } - }; + const { control, getValues } = useFormContext<{ + hostname: string; + }>(); return (
@@ -128,40 +38,12 @@ export const SettingsHostname = () => { )} /> - - - {isDefined(getHostnameDetailsData?.getHostnameDetails?.hostname) && ( -
-          {getHostnameDetailsData.getHostnameDetails.hostname} CNAME
-          twenty-main.com
-        
- )} - {getHostnameDetailsData?.getHostnameDetails && - getHostnameDetailsData.getHostnameDetails.ownershipVerifications.map( - (ownershipVerification) => { - if ( - ownershipVerification.__typename === - 'CustomHostnameOwnershipVerificationTxt' - ) { - return ( -
-                  {ownershipVerification.name} TXT {ownershipVerification.value}
-                
- ); - } - - if ( - ownershipVerification.__typename === - 'CustomHostnameOwnershipVerificationHttp' - ) { - return ( -
-                  {ownershipVerification.url} HTTP {ownershipVerification.body}
-                
- ); - } - return <>; - }, + {getHostnameDetailsData?.getCustomHostnameDetails && + getValues('hostname') === + getHostnameDetailsData?.getCustomHostnameDetails?.hostname && ( + )}
); diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsHostnameEffect.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsHostnameEffect.tsx index 23dac27bb..eb852dab8 100644 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsHostnameEffect.tsx +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsHostnameEffect.tsx @@ -2,10 +2,10 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared'; -import { useGetHostnameDetailsQuery } from '~/generated/graphql'; +import { useGetCustomHostnameDetailsQuery } from '~/generated/graphql'; export const SettingsHostnameEffect = () => { - const { refetch } = useGetHostnameDetailsQuery(); + const { refetch } = useGetCustomHostnameDetailsQuery(); const currentWorkspace = useRecoilValue(currentWorkspaceState); diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsHostnameRecords.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsHostnameRecords.tsx new file mode 100644 index 000000000..0b18295f4 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsHostnameRecords.tsx @@ -0,0 +1,75 @@ +import { TableRow } from '@/ui/layout/table/components/TableRow'; +import { TableHeader } from '@/ui/layout/table/components/TableHeader'; +import { Separator } from '@/settings/components/Separator'; +import { TableBody } from '@/ui/layout/table/components/TableBody'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { TextInputV2 } from '@/ui/input/components/TextInputV2'; +import { Table } from '@/ui/layout/table/components/Table'; +import { CustomHostnameDetails } from '~/generated/graphql'; + +export const SettingsHostnameRecords = ({ + records, +}: { + records: CustomHostnameDetails['records']; +}) => { + return ( + + + Name + Record Type + Value + Validation Type + Status + + + + {records.map((record) => { + return ( + + + + + + + + + + + + + + + + + + ); + })} + +
+ ); +}; diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 6e292b64d..2e946feae 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -30,7 +30,7 @@ "cache-manager": "^5.4.0", "cache-manager-redis-yet": "^4.1.2", "class-validator": "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch", - "cloudflare": "^3.5.0", + "cloudflare": "^4.0.0", "connect-redis": "^7.1.1", "express-session": "^1.18.1", "graphql-middleware": "^6.1.35", diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/dtos/custom-hostname-details.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/dtos/custom-hostname-details.ts index c133b2357..23636701a 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/dtos/custom-hostname-details.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/dtos/custom-hostname-details.ts @@ -1,48 +1,23 @@ -import { createUnionType, Field, ObjectType } from '@nestjs/graphql'; +import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() -class CustomHostnameOwnershipVerificationTxt { +class CustomHostnameVerification { @Field(() => String) - type: 'txt'; + validationType: 'ownership' | 'ssl' | 'redirection'; @Field(() => String) - name: string; + type: 'txt' | 'cname'; + + @Field(() => String) + key: string; + + @Field(() => String) + status: string; @Field(() => String) value: string; } -@ObjectType() -class CustomHostnameOwnershipVerificationHttp { - @Field() - type: 'http'; - - @Field(() => String) - body: string; - - @Field(() => String) - url: string; -} - -const CustomHostnameOwnershipVerification = createUnionType({ - name: 'OwnershipVerification', - types: () => - [ - CustomHostnameOwnershipVerificationTxt, - CustomHostnameOwnershipVerificationHttp, - ] as const, - resolveType(value) { - if ('type' in value && value.type === 'txt') { - return CustomHostnameOwnershipVerificationTxt; - } - if ('type' in value && value.type === 'http') { - return CustomHostnameOwnershipVerificationHttp; - } - - return null; - }, -}); - @ObjectType() export class CustomHostnameDetails { @Field(() => String) @@ -51,25 +26,6 @@ export class CustomHostnameDetails { @Field(() => String) hostname: string; - @Field(() => [CustomHostnameOwnershipVerification]) - ownershipVerifications: Array; - - @Field(() => String, { nullable: true }) - status?: - | 'active' - | 'pending' - | 'active_redeploying' - | 'moved' - | 'pending_deletion' - | 'deleted' - | 'pending_blocked' - | 'pending_migration' - | 'pending_provisioned' - | 'test_pending' - | 'test_active' - | 'test_active_apex' - | 'test_blocked' - | 'test_failed' - | 'provisioned' - | 'blocked'; + @Field(() => [CustomHostnameVerification]) + records: Array; } diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts index 6532dc970..cdc45bd07 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts @@ -283,44 +283,59 @@ export class DomainManagerService { return { id: response.result[0].id, hostname: response.result[0].hostname, - status: response.result[0].status, - ownershipVerifications: [ + records: [ response.result[0].ownership_verification, - response.result[0].ownership_verification_http, - ].reduce( - (acc, ownershipVerification) => { - if (!ownershipVerification) return acc; + ...(response.result[0].ssl?.validation_records ?? []), + ] + .map( + (record: Record) => { + if (!record) return; - if ( - 'http_body' in ownershipVerification && - 'http_url' in ownershipVerification && - ownershipVerification.http_body && - ownershipVerification.http_url - ) { - acc.push({ - type: 'http', - body: ownershipVerification.http_body, - url: ownershipVerification.http_url, - }); - } + 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 ?? 'pending', + key: record.txt_name, + value: record.txt_value, + }; + } - if ( - 'type' in ownershipVerification && - ownershipVerification.type === 'txt' && - ownershipVerification.value && - ownershipVerification.name - ) { - acc.push({ - type: 'txt', - value: ownershipVerification.value, - name: ownershipVerification.name, - }); - } - - return acc; - }, - [] as CustomHostnameDetails['ownershipVerifications'], - ), + 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: + response.result[0].verification_errors?.[0] === + 'custom hostname does not CNAME to this zone.' + ? 'error' + : 'success', + key: response.result[0].hostname, + value: this.getFrontUrl().hostname, + }, + ]), }; } diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts index 870dbb72b..e627a4a7e 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts @@ -2,7 +2,7 @@ import { Field, InputType } from '@nestjs/graphql'; -import { IsOptional, IsBoolean, IsString } from 'class-validator'; +import { IsOptional, IsString } from 'class-validator'; @InputType() export class GetAuthorizationUrlInput { diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index 6305579db..f80383249 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -222,7 +222,7 @@ export class WorkspaceResolver { @Query(() => CustomHostnameDetails, { nullable: true }) @UseGuards(WorkspaceAuthGuard) - async getHostnameDetails( + async getCustomHostnameDetails( @AuthWorkspace() { hostname }: Workspace, ): Promise { if (!hostname) return undefined; diff --git a/yarn.lock b/yarn.lock index cd3c65c61..d97223f18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17590,13 +17590,6 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:^6.9.7": - version: 6.9.17 - resolution: "@types/qs@npm:6.9.17" - checksum: 10c0/a183fa0b3464267f8f421e2d66d960815080e8aab12b9aadab60479ba84183b1cdba8f4eff3c06f76675a8e42fe6a3b1313ea76c74f2885c3e25d32499c17d1b - languageName: node - linkType: hard - "@types/range-parser@npm:*": version: 1.2.7 resolution: "@types/range-parser@npm:1.2.7" @@ -23122,21 +23115,18 @@ __metadata: languageName: node linkType: hard -"cloudflare@npm:^3.5.0": - version: 3.5.0 - resolution: "cloudflare@npm:3.5.0" +"cloudflare@npm:^4.0.0": + version: 4.0.0 + resolution: "cloudflare@npm:4.0.0" dependencies: "@types/node": "npm:^18.11.18" "@types/node-fetch": "npm:^2.6.4" - "@types/qs": "npm:^6.9.7" abort-controller: "npm:^3.0.0" agentkeepalive: "npm:^4.2.1" form-data-encoder: "npm:1.7.2" formdata-node: "npm:^4.3.2" node-fetch: "npm:^2.6.7" - qs: "npm:^6.10.3" - web-streams-polyfill: "npm:^3.2.1" - checksum: 10c0/bb48ff68a4f5b7e945ceec570e7e17251ad167d8d2dadf8099fd74df46582356382b8cd3f62a9da74cd3a208f8f5181a40c561bc5873592d6a4930458c0e7b86 + checksum: 10c0/d83a4d0544de5794935acb802c875c6f718713d8c7613b2a74d6145b922f18415e514cc57bbef9249726d83c0788ac8a72517a69efb1fd5b5af58b654403c375 languageName: node linkType: hard @@ -40591,15 +40581,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.3": - version: 6.13.1 - resolution: "qs@npm:6.13.1" - dependencies: - side-channel: "npm:^1.0.6" - checksum: 10c0/5ef527c0d62ffca5501322f0832d800ddc78eeb00da3b906f1b260ca0492721f8cdc13ee4b8fd8ac314a6ec37b948798c7b603ccc167e954088df392092f160c - languageName: node - linkType: hard - "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3" @@ -45936,7 +45917,7 @@ __metadata: cache-manager: "npm:^5.4.0" cache-manager-redis-yet: "npm:^4.1.2" class-validator: "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch" - cloudflare: "npm:^3.5.0" + cloudflare: "npm:^4.0.0" connect-redis: "npm:^7.1.1" express-session: "npm:^1.18.1" graphql-middleware: "npm:^6.1.35"