feat(custom-domain): enable UI for custom domain (#10062)

This commit is contained in:
Antoine Moreaux
2025-02-10 09:43:13 +01:00
committed by GitHub
parent f8c653f153
commit 1b98f40f17
13 changed files with 305 additions and 343 deletions

View File

@ -389,21 +389,15 @@ export type CustomHostnameDetails = {
__typename?: 'CustomHostnameDetails';
hostname: Scalars['String']['output'];
id: Scalars['String']['output'];
ownershipVerifications: Array<OwnershipVerification>;
status?: Maybe<Scalars['String']['output']>;
records: Array<CustomHostnameVerification>;
};
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<AnalyticsTinybirdJwtMap>;
canImpersonate: Scalars['Boolean']['output'];
createdAt: Scalars['DateTime']['output'];
currentWorkspace: Workspace;
currentUserWorkspace?: Maybe<UserWorkspace>;
currentWorkspace?: Maybe<Workspace>;
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
deletedAt?: Maybe<Scalars['DateTime']['output']>;
disabled?: Maybe<Scalars['Boolean']['output']>;
@ -2060,6 +2063,7 @@ export type UserWorkspace = {
createdAt: Scalars['DateTime']['output'];
deletedAt?: Maybe<Scalars['DateTime']['output']>;
id: Scalars['UUID']['output'];
settingsPermissions?: Maybe<Array<SettingsFeatures>>;
updatedAt: Scalars['DateTime']['output'];
user: User;
userId: Scalars['String']['output'];

View File

@ -326,21 +326,15 @@ export type CustomHostnameDetails = {
__typename?: 'CustomHostnameDetails';
hostname: Scalars['String'];
id: Scalars['String'];
ownershipVerifications: Array<OwnershipVerification>;
status?: Maybe<Scalars['String']>;
records: Array<CustomHostnameVerification>;
};
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<WorkspaceInvitation>;
getAvailablePackages: Scalars['JSON'];
getCustomHostnameDetails?: Maybe<CustomHostnameDetails>;
getEnvironmentVariablesGrouped: EnvironmentVariablesOutput;
getHostnameDetails?: Maybe<CustomHostnameDetails>;
getPostgresCredentials?: Maybe<PostgresCredentials>;
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<typeof useUploadWorkspaceLogoMutation>;
export type UploadWorkspaceLogoMutationResult = Apollo.MutationResult<UploadWorkspaceLogoMutation>;
export type UploadWorkspaceLogoMutationOptions = Apollo.BaseMutationOptions<UploadWorkspaceLogoMutation, UploadWorkspaceLogoMutationVariables>;
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<GetHostnameDetailsQuery, GetHostnameDetailsQueryVariables>) {
export function useGetCustomHostnameDetailsQuery(baseOptions?: Apollo.QueryHookOptions<GetCustomHostnameDetailsQuery, GetCustomHostnameDetailsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetHostnameDetailsQuery, GetHostnameDetailsQueryVariables>(GetHostnameDetailsDocument, options);
return Apollo.useQuery<GetCustomHostnameDetailsQuery, GetCustomHostnameDetailsQueryVariables>(GetCustomHostnameDetailsDocument, options);
}
export function useGetHostnameDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetHostnameDetailsQuery, GetHostnameDetailsQueryVariables>) {
export function useGetCustomHostnameDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetCustomHostnameDetailsQuery, GetCustomHostnameDetailsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetHostnameDetailsQuery, GetHostnameDetailsQueryVariables>(GetHostnameDetailsDocument, options);
return Apollo.useLazyQuery<GetCustomHostnameDetailsQuery, GetCustomHostnameDetailsQueryVariables>(GetCustomHostnameDetailsDocument, options);
}
export type GetHostnameDetailsQueryHookResult = ReturnType<typeof useGetHostnameDetailsQuery>;
export type GetHostnameDetailsLazyQueryHookResult = ReturnType<typeof useGetHostnameDetailsLazyQuery>;
export type GetHostnameDetailsQueryResult = Apollo.QueryResult<GetHostnameDetailsQuery, GetHostnameDetailsQueryVariables>;
export type GetCustomHostnameDetailsQueryHookResult = ReturnType<typeof useGetCustomHostnameDetailsQuery>;
export type GetCustomHostnameDetailsLazyQueryHookResult = ReturnType<typeof useGetCustomHostnameDetailsLazyQuery>;
export type GetCustomHostnameDetailsQueryResult = Apollo.QueryResult<GetCustomHostnameDetailsQuery, GetCustomHostnameDetailsQueryVariables>;
export const GetWorkspaceFromInviteHashDocument = gql`
query GetWorkspaceFromInviteHash($inviteHash: String!) {
findWorkspaceFromInviteHash(inviteHash: $inviteHash) {

View File

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

View File

@ -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 (
<SubMenuTopBarContainer
title={t`Domain`}
@ -128,7 +196,8 @@ export const SettingsDomain = () => {
<SaveAndCancelButtons
isSaveDisabled={
!form.formState.isValid ||
subdomainValue === currentWorkspace?.subdomain
(subdomainValue === currentWorkspace?.subdomain &&
hostnameValue === currentWorkspace?.hostname)
}
onCancel={() => navigate(SettingsPath.Workspace)}
onSave={handleSave}
@ -138,15 +207,15 @@ export const SettingsDomain = () => {
<SettingsPageContainer>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<FormProvider {...form}>
{(!currentWorkspace?.hostname || !isCustomDomainEnabled) && (
<SettingsSubdomain />
)}
{isCustomDomainEnabled && (
<>
<SettingsHostnameEffect />
<SettingsHostname />
</>
)}
{(!currentWorkspace?.hostname || !isCustomDomainEnabled) && (
<SettingsSubdomain />
)}
</FormProvider>
</SettingsPageContainer>
</SubMenuTopBarContainer>

View File

@ -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<typeof validationSchema>;
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<Form>({
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 (
<Section>
@ -128,40 +38,12 @@ export const SettingsHostname = () => {
)}
/>
</StyledDomainFromWrapper>
<Button onClick={handleSubmit(handleSave)} title={'save'}></Button>
<Button onClick={handleSubmit(handleDelete)} title={'delete'}></Button>
{isDefined(getHostnameDetailsData?.getHostnameDetails?.hostname) && (
<pre>
{getHostnameDetailsData.getHostnameDetails.hostname} CNAME
twenty-main.com
</pre>
)}
{getHostnameDetailsData?.getHostnameDetails &&
getHostnameDetailsData.getHostnameDetails.ownershipVerifications.map(
(ownershipVerification) => {
if (
ownershipVerification.__typename ===
'CustomHostnameOwnershipVerificationTxt'
) {
return (
<pre>
{ownershipVerification.name} TXT {ownershipVerification.value}
</pre>
);
}
if (
ownershipVerification.__typename ===
'CustomHostnameOwnershipVerificationHttp'
) {
return (
<pre>
{ownershipVerification.url} HTTP {ownershipVerification.body}
</pre>
);
}
return <></>;
},
{getHostnameDetailsData?.getCustomHostnameDetails &&
getValues('hostname') ===
getHostnameDetailsData?.getCustomHostnameDetails?.hostname && (
<SettingsHostnameRecords
records={getHostnameDetailsData.getCustomHostnameDetails.records}
/>
)}
</Section>
);

View File

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

View File

@ -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 (
<Table>
<TableRow>
<TableHeader>Name</TableHeader>
<TableHeader>Record Type</TableHeader>
<TableHeader>Value</TableHeader>
<TableHeader>Validation Type</TableHeader>
<TableHeader>Status</TableHeader>
</TableRow>
<Separator></Separator>
<TableBody>
{records.map((record) => {
return (
<TableRow>
<TableCell>
<TextInputV2
value={record.key}
type="text"
disabled
sizeVariant="md"
/>
</TableCell>
<TableCell>
<TextInputV2
value={record.type.toUpperCase()}
type="text"
disabled
sizeVariant="md"
/>
</TableCell>
<TableCell>
<TextInputV2
value={record.value}
type="text"
disabled
sizeVariant="md"
/>
</TableCell>
<TableCell>
<TextInputV2
value={record.validationType}
type="text"
disabled
sizeVariant="md"
/>
</TableCell>
<TableCell>
<TextInputV2
value={record.status}
type="text"
disabled
sizeVariant="md"
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
};

View File

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

View File

@ -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<typeof CustomHostnameOwnershipVerification>;
@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<CustomHostnameVerification>;
}

View File

@ -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<CustomHostnameDetails['records'][0] | undefined>(
(record: Record<string, string>) => {
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,
},
]),
};
}

View File

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

View File

@ -222,7 +222,7 @@ export class WorkspaceResolver {
@Query(() => CustomHostnameDetails, { nullable: true })
@UseGuards(WorkspaceAuthGuard)
async getHostnameDetails(
async getCustomHostnameDetails(
@AuthWorkspace() { hostname }: Workspace,
): Promise<CustomHostnameDetails | undefined> {
if (!hostname) return undefined;

View File

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