feat(custom-domain): enable UI for custom domain (#10062)
This commit is contained in:
@ -389,21 +389,15 @@ export type CustomHostnameDetails = {
|
|||||||
__typename?: 'CustomHostnameDetails';
|
__typename?: 'CustomHostnameDetails';
|
||||||
hostname: Scalars['String']['output'];
|
hostname: Scalars['String']['output'];
|
||||||
id: Scalars['String']['output'];
|
id: Scalars['String']['output'];
|
||||||
ownershipVerifications: Array<OwnershipVerification>;
|
records: Array<CustomHostnameVerification>;
|
||||||
status?: Maybe<Scalars['String']['output']>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomHostnameOwnershipVerificationHttp = {
|
export type CustomHostnameVerification = {
|
||||||
__typename?: 'CustomHostnameOwnershipVerificationHttp';
|
__typename?: 'CustomHostnameVerification';
|
||||||
body: Scalars['String']['output'];
|
key: Scalars['String']['output'];
|
||||||
type: Scalars['String']['output'];
|
status: Scalars['String']['output'];
|
||||||
url: Scalars['String']['output'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CustomHostnameOwnershipVerificationTxt = {
|
|
||||||
__typename?: 'CustomHostnameOwnershipVerificationTxt';
|
|
||||||
name: Scalars['String']['output'];
|
|
||||||
type: Scalars['String']['output'];
|
type: Scalars['String']['output'];
|
||||||
|
validationType: Scalars['String']['output'];
|
||||||
value: Scalars['String']['output'];
|
value: Scalars['String']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1313,8 +1307,6 @@ export type OnboardingStepSuccess = {
|
|||||||
success: Scalars['Boolean']['output'];
|
success: Scalars['Boolean']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OwnershipVerification = CustomHostnameOwnershipVerificationHttp | CustomHostnameOwnershipVerificationTxt;
|
|
||||||
|
|
||||||
export type PageInfo = {
|
export type PageInfo = {
|
||||||
__typename?: 'PageInfo';
|
__typename?: 'PageInfo';
|
||||||
/** The cursor of the last returned record. */
|
/** The cursor of the last returned record. */
|
||||||
@ -1755,6 +1747,16 @@ export enum ServerlessFunctionSyncStatus {
|
|||||||
READY = 'READY'
|
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 = {
|
export type SetupOidcSsoInput = {
|
||||||
clientID: Scalars['String']['input'];
|
clientID: Scalars['String']['input'];
|
||||||
clientSecret: Scalars['String']['input'];
|
clientSecret: Scalars['String']['input'];
|
||||||
@ -1985,7 +1987,8 @@ export type User = {
|
|||||||
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
|
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
|
||||||
canImpersonate: Scalars['Boolean']['output'];
|
canImpersonate: Scalars['Boolean']['output'];
|
||||||
createdAt: Scalars['DateTime']['output'];
|
createdAt: Scalars['DateTime']['output'];
|
||||||
currentWorkspace: Workspace;
|
currentUserWorkspace?: Maybe<UserWorkspace>;
|
||||||
|
currentWorkspace?: Maybe<Workspace>;
|
||||||
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
|
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
|
||||||
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||||
disabled?: Maybe<Scalars['Boolean']['output']>;
|
disabled?: Maybe<Scalars['Boolean']['output']>;
|
||||||
@ -2060,6 +2063,7 @@ export type UserWorkspace = {
|
|||||||
createdAt: Scalars['DateTime']['output'];
|
createdAt: Scalars['DateTime']['output'];
|
||||||
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||||
id: Scalars['UUID']['output'];
|
id: Scalars['UUID']['output'];
|
||||||
|
settingsPermissions?: Maybe<Array<SettingsFeatures>>;
|
||||||
updatedAt: Scalars['DateTime']['output'];
|
updatedAt: Scalars['DateTime']['output'];
|
||||||
user: User;
|
user: User;
|
||||||
userId: Scalars['String']['output'];
|
userId: Scalars['String']['output'];
|
||||||
|
|||||||
@ -326,21 +326,15 @@ export type CustomHostnameDetails = {
|
|||||||
__typename?: 'CustomHostnameDetails';
|
__typename?: 'CustomHostnameDetails';
|
||||||
hostname: Scalars['String'];
|
hostname: Scalars['String'];
|
||||||
id: Scalars['String'];
|
id: Scalars['String'];
|
||||||
ownershipVerifications: Array<OwnershipVerification>;
|
records: Array<CustomHostnameVerification>;
|
||||||
status?: Maybe<Scalars['String']>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomHostnameOwnershipVerificationHttp = {
|
export type CustomHostnameVerification = {
|
||||||
__typename?: 'CustomHostnameOwnershipVerificationHttp';
|
__typename?: 'CustomHostnameVerification';
|
||||||
body: Scalars['String'];
|
key: Scalars['String'];
|
||||||
type: Scalars['String'];
|
status: Scalars['String'];
|
||||||
url: Scalars['String'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CustomHostnameOwnershipVerificationTxt = {
|
|
||||||
__typename?: 'CustomHostnameOwnershipVerificationTxt';
|
|
||||||
name: Scalars['String'];
|
|
||||||
type: Scalars['String'];
|
type: Scalars['String'];
|
||||||
|
validationType: Scalars['String'];
|
||||||
value: Scalars['String'];
|
value: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1181,8 +1175,6 @@ export type OnboardingStepSuccess = {
|
|||||||
success: Scalars['Boolean'];
|
success: Scalars['Boolean'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OwnershipVerification = CustomHostnameOwnershipVerificationHttp | CustomHostnameOwnershipVerificationTxt;
|
|
||||||
|
|
||||||
export type PageInfo = {
|
export type PageInfo = {
|
||||||
__typename?: 'PageInfo';
|
__typename?: 'PageInfo';
|
||||||
/** The cursor of the last returned record. */
|
/** The cursor of the last returned record. */
|
||||||
@ -1246,8 +1238,8 @@ export type Query = {
|
|||||||
findWorkspaceFromInviteHash: Workspace;
|
findWorkspaceFromInviteHash: Workspace;
|
||||||
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
||||||
getAvailablePackages: Scalars['JSON'];
|
getAvailablePackages: Scalars['JSON'];
|
||||||
|
getCustomHostnameDetails?: Maybe<CustomHostnameDetails>;
|
||||||
getEnvironmentVariablesGrouped: EnvironmentVariablesOutput;
|
getEnvironmentVariablesGrouped: EnvironmentVariablesOutput;
|
||||||
getHostnameDetails?: Maybe<CustomHostnameDetails>;
|
|
||||||
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
||||||
getProductPrices: BillingProductPricesOutput;
|
getProductPrices: BillingProductPricesOutput;
|
||||||
getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput;
|
getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput;
|
||||||
@ -2432,10 +2424,10 @@ export type UploadWorkspaceLogoMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type UploadWorkspaceLogoMutation = { __typename?: 'Mutation', uploadWorkspaceLogo: string };
|
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<{
|
export type GetWorkspaceFromInviteHashQueryVariables = Exact<{
|
||||||
inviteHash: Scalars['String'];
|
inviteHash: Scalars['String'];
|
||||||
@ -4868,53 +4860,47 @@ export function useUploadWorkspaceLogoMutation(baseOptions?: Apollo.MutationHook
|
|||||||
export type UploadWorkspaceLogoMutationHookResult = ReturnType<typeof useUploadWorkspaceLogoMutation>;
|
export type UploadWorkspaceLogoMutationHookResult = ReturnType<typeof useUploadWorkspaceLogoMutation>;
|
||||||
export type UploadWorkspaceLogoMutationResult = Apollo.MutationResult<UploadWorkspaceLogoMutation>;
|
export type UploadWorkspaceLogoMutationResult = Apollo.MutationResult<UploadWorkspaceLogoMutation>;
|
||||||
export type UploadWorkspaceLogoMutationOptions = Apollo.BaseMutationOptions<UploadWorkspaceLogoMutation, UploadWorkspaceLogoMutationVariables>;
|
export type UploadWorkspaceLogoMutationOptions = Apollo.BaseMutationOptions<UploadWorkspaceLogoMutation, UploadWorkspaceLogoMutationVariables>;
|
||||||
export const GetHostnameDetailsDocument = gql`
|
export const GetCustomHostnameDetailsDocument = gql`
|
||||||
query GetHostnameDetails {
|
query GetCustomHostnameDetails {
|
||||||
getHostnameDetails {
|
getCustomHostnameDetails {
|
||||||
hostname
|
hostname
|
||||||
ownershipVerifications {
|
records {
|
||||||
... on CustomHostnameOwnershipVerificationTxt {
|
type
|
||||||
type
|
key
|
||||||
name
|
value
|
||||||
value
|
validationType
|
||||||
}
|
status
|
||||||
... on CustomHostnameOwnershipVerificationHttp {
|
|
||||||
type
|
|
||||||
body
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
status
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __useGetHostnameDetailsQuery__
|
* __useGetCustomHostnameDetailsQuery__
|
||||||
*
|
*
|
||||||
* To run a query within a React component, call `useGetHostnameDetailsQuery` and pass it any options that fit your needs.
|
* To run a query within a React component, call `useGetCustomHostnameDetailsQuery` 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
|
* 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.
|
* 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;
|
* @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
|
* @example
|
||||||
* const { data, loading, error } = useGetHostnameDetailsQuery({
|
* const { data, loading, error } = useGetCustomHostnameDetailsQuery({
|
||||||
* variables: {
|
* variables: {
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
export function useGetHostnameDetailsQuery(baseOptions?: Apollo.QueryHookOptions<GetHostnameDetailsQuery, GetHostnameDetailsQueryVariables>) {
|
export function useGetCustomHostnameDetailsQuery(baseOptions?: Apollo.QueryHookOptions<GetCustomHostnameDetailsQuery, GetCustomHostnameDetailsQueryVariables>) {
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
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}
|
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 GetCustomHostnameDetailsQueryHookResult = ReturnType<typeof useGetCustomHostnameDetailsQuery>;
|
||||||
export type GetHostnameDetailsLazyQueryHookResult = ReturnType<typeof useGetHostnameDetailsLazyQuery>;
|
export type GetCustomHostnameDetailsLazyQueryHookResult = ReturnType<typeof useGetCustomHostnameDetailsLazyQuery>;
|
||||||
export type GetHostnameDetailsQueryResult = Apollo.QueryResult<GetHostnameDetailsQuery, GetHostnameDetailsQueryVariables>;
|
export type GetCustomHostnameDetailsQueryResult = Apollo.QueryResult<GetCustomHostnameDetailsQuery, GetCustomHostnameDetailsQueryVariables>;
|
||||||
export const GetWorkspaceFromInviteHashDocument = gql`
|
export const GetWorkspaceFromInviteHashDocument = gql`
|
||||||
query GetWorkspaceFromInviteHash($inviteHash: String!) {
|
query GetWorkspaceFromInviteHash($inviteHash: String!) {
|
||||||
findWorkspaceFromInviteHash(inviteHash: $inviteHash) {
|
findWorkspaceFromInviteHash(inviteHash: $inviteHash) {
|
||||||
|
|||||||
@ -1,22 +1,16 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
export const GET_HOSTNAME_DETAILS = gql`
|
export const GET_CUSTOM_HOSTNAME_DETAILS = gql`
|
||||||
query GetHostnameDetails {
|
query GetCustomHostnameDetails {
|
||||||
getHostnameDetails {
|
getCustomHostnameDetails {
|
||||||
hostname
|
hostname
|
||||||
ownershipVerifications {
|
records {
|
||||||
... on CustomHostnameOwnershipVerificationTxt {
|
type
|
||||||
type
|
key
|
||||||
name
|
value
|
||||||
value
|
validationType
|
||||||
}
|
status
|
||||||
... on CustomHostnameOwnershipVerificationHttp {
|
|
||||||
type
|
|
||||||
body
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
status
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { ApolloError } from '@apollo/client';
|
import { ApolloError } from '@apollo/client';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import {
|
||||||
|
CurrentWorkspace,
|
||||||
|
currentWorkspaceState,
|
||||||
|
} from '@/auth/states/currentWorkspaceState';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
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 { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsHostnameEffect } from '~/pages/settings/workspace/SettingsHostnameEffect';
|
import { SettingsHostnameEffect } from '~/pages/settings/workspace/SettingsHostnameEffect';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
export const SettingsDomain = () => {
|
export const SettingsDomain = () => {
|
||||||
const navigate = useNavigateSettings();
|
const navigate = useNavigateSettings();
|
||||||
@ -36,6 +40,17 @@ export const SettingsDomain = () => {
|
|||||||
.regex(/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/, {
|
.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`,
|
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();
|
.required();
|
||||||
|
|
||||||
@ -53,30 +68,62 @@ export const SettingsDomain = () => {
|
|||||||
|
|
||||||
const form = useForm<{
|
const form = useForm<{
|
||||||
subdomain: string;
|
subdomain: string;
|
||||||
|
hostname: string | null;
|
||||||
}>({
|
}>({
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
delayError: 500,
|
delayError: 500,
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
subdomain: currentWorkspace?.subdomain ?? '',
|
subdomain: currentWorkspace?.subdomain ?? '',
|
||||||
|
hostname: currentWorkspace?.hostname ?? null,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(validationSchema),
|
resolver: zodResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const subdomainValue = form.watch('subdomain');
|
const subdomainValue = form.watch('subdomain');
|
||||||
|
const hostnameValue = form.watch('hostname');
|
||||||
|
|
||||||
const handleSave = async () => {
|
const updateHostname = (
|
||||||
const values = form.getValues();
|
hostname: string | null | undefined,
|
||||||
|
currentWorkspace: CurrentWorkspace,
|
||||||
if (!values || !form.formState.isValid || !currentWorkspace) {
|
) => {
|
||||||
return enqueueSnackBar(t`Invalid form values`, {
|
updateWorkspace({
|
||||||
variant: SnackBarVariant.Error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateWorkspace({
|
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
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) => {
|
onError: (error) => {
|
||||||
@ -98,11 +145,11 @@ export const SettingsDomain = () => {
|
|||||||
|
|
||||||
currentUrl.hostname = new URL(
|
currentUrl.hostname = new URL(
|
||||||
currentWorkspace.workspaceUrls.subdomainUrl,
|
currentWorkspace.workspaceUrls.subdomainUrl,
|
||||||
).hostname.replace(currentWorkspace.subdomain, values.subdomain);
|
).hostname.replace(currentWorkspace.subdomain, subdomain);
|
||||||
|
|
||||||
setCurrentWorkspace({
|
setCurrentWorkspace({
|
||||||
...currentWorkspace,
|
...currentWorkspace,
|
||||||
subdomain: values.subdomain,
|
subdomain,
|
||||||
});
|
});
|
||||||
|
|
||||||
redirectToWorkspaceDomain(currentUrl.toString());
|
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 (
|
return (
|
||||||
<SubMenuTopBarContainer
|
<SubMenuTopBarContainer
|
||||||
title={t`Domain`}
|
title={t`Domain`}
|
||||||
@ -128,7 +196,8 @@ export const SettingsDomain = () => {
|
|||||||
<SaveAndCancelButtons
|
<SaveAndCancelButtons
|
||||||
isSaveDisabled={
|
isSaveDisabled={
|
||||||
!form.formState.isValid ||
|
!form.formState.isValid ||
|
||||||
subdomainValue === currentWorkspace?.subdomain
|
(subdomainValue === currentWorkspace?.subdomain &&
|
||||||
|
hostnameValue === currentWorkspace?.hostname)
|
||||||
}
|
}
|
||||||
onCancel={() => navigate(SettingsPath.Workspace)}
|
onCancel={() => navigate(SettingsPath.Workspace)}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
@ -138,15 +207,15 @@ export const SettingsDomain = () => {
|
|||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
|
{(!currentWorkspace?.hostname || !isCustomDomainEnabled) && (
|
||||||
|
<SettingsSubdomain />
|
||||||
|
)}
|
||||||
{isCustomDomainEnabled && (
|
{isCustomDomainEnabled && (
|
||||||
<>
|
<>
|
||||||
<SettingsHostnameEffect />
|
<SettingsHostnameEffect />
|
||||||
<SettingsHostname />
|
<SettingsHostname />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(!currentWorkspace?.hostname || !isCustomDomainEnabled) && (
|
|
||||||
<SettingsSubdomain />
|
|
||||||
)}
|
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
</SubMenuTopBarContainer>
|
</SubMenuTopBarContainer>
|
||||||
|
|||||||
@ -1,35 +1,10 @@
|
|||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
|
||||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import { useRecoilState } from 'recoil';
|
import { H2Title, Section } from 'twenty-ui';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { useGetCustomHostnameDetailsQuery } from '~/generated/graphql';
|
||||||
import { Button, H2Title, Section } from 'twenty-ui';
|
import { SettingsHostnameRecords } from '~/pages/settings/workspace/SettingsHostnameRecords';
|
||||||
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>;
|
|
||||||
|
|
||||||
const StyledDomainFromWrapper = styled.div`
|
const StyledDomainFromWrapper = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -37,78 +12,13 @@ const StyledDomainFromWrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsHostname = () => {
|
export const SettingsHostname = () => {
|
||||||
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
const { data: getHostnameDetailsData } = useGetCustomHostnameDetailsQuery();
|
||||||
const { data: getHostnameDetailsData } = useGetHostnameDetailsQuery();
|
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
const { control, getValues } = useFormContext<{
|
||||||
currentWorkspaceState,
|
hostname: string;
|
||||||
);
|
}>();
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section>
|
||||||
@ -128,40 +38,12 @@ export const SettingsHostname = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</StyledDomainFromWrapper>
|
</StyledDomainFromWrapper>
|
||||||
<Button onClick={handleSubmit(handleSave)} title={'save'}></Button>
|
{getHostnameDetailsData?.getCustomHostnameDetails &&
|
||||||
<Button onClick={handleSubmit(handleDelete)} title={'delete'}></Button>
|
getValues('hostname') ===
|
||||||
{isDefined(getHostnameDetailsData?.getHostnameDetails?.hostname) && (
|
getHostnameDetailsData?.getCustomHostnameDetails?.hostname && (
|
||||||
<pre>
|
<SettingsHostnameRecords
|
||||||
{getHostnameDetailsData.getHostnameDetails.hostname} CNAME
|
records={getHostnameDetailsData.getCustomHostnameDetails.records}
|
||||||
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 <></>;
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { useGetHostnameDetailsQuery } from '~/generated/graphql';
|
import { useGetCustomHostnameDetailsQuery } from '~/generated/graphql';
|
||||||
|
|
||||||
export const SettingsHostnameEffect = () => {
|
export const SettingsHostnameEffect = () => {
|
||||||
const { refetch } = useGetHostnameDetailsQuery();
|
const { refetch } = useGetCustomHostnameDetailsQuery();
|
||||||
|
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -30,7 +30,7 @@
|
|||||||
"cache-manager": "^5.4.0",
|
"cache-manager": "^5.4.0",
|
||||||
"cache-manager-redis-yet": "^4.1.2",
|
"cache-manager-redis-yet": "^4.1.2",
|
||||||
"class-validator": "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch",
|
"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",
|
"connect-redis": "^7.1.1",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"graphql-middleware": "^6.1.35",
|
"graphql-middleware": "^6.1.35",
|
||||||
|
|||||||
@ -1,48 +1,23 @@
|
|||||||
import { createUnionType, Field, ObjectType } from '@nestjs/graphql';
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
class CustomHostnameOwnershipVerificationTxt {
|
class CustomHostnameVerification {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
type: 'txt';
|
validationType: 'ownership' | 'ssl' | 'redirection';
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
name: string;
|
type: 'txt' | 'cname';
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
status: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
value: 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()
|
@ObjectType()
|
||||||
export class CustomHostnameDetails {
|
export class CustomHostnameDetails {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@ -51,25 +26,6 @@ export class CustomHostnameDetails {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
|
||||||
@Field(() => [CustomHostnameOwnershipVerification])
|
@Field(() => [CustomHostnameVerification])
|
||||||
ownershipVerifications: Array<typeof CustomHostnameOwnershipVerification>;
|
records: Array<CustomHostnameVerification>;
|
||||||
|
|
||||||
@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';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -283,44 +283,59 @@ export class DomainManagerService {
|
|||||||
return {
|
return {
|
||||||
id: response.result[0].id,
|
id: response.result[0].id,
|
||||||
hostname: response.result[0].hostname,
|
hostname: response.result[0].hostname,
|
||||||
status: response.result[0].status,
|
records: [
|
||||||
ownershipVerifications: [
|
|
||||||
response.result[0].ownership_verification,
|
response.result[0].ownership_verification,
|
||||||
response.result[0].ownership_verification_http,
|
...(response.result[0].ssl?.validation_records ?? []),
|
||||||
].reduce(
|
]
|
||||||
(acc, ownershipVerification) => {
|
.map<CustomHostnameDetails['records'][0] | undefined>(
|
||||||
if (!ownershipVerification) return acc;
|
(record: Record<string, string>) => {
|
||||||
|
if (!record) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
'http_body' in ownershipVerification &&
|
'txt_name' in record &&
|
||||||
'http_url' in ownershipVerification &&
|
'txt_value' in record &&
|
||||||
ownershipVerification.http_body &&
|
record.txt_name &&
|
||||||
ownershipVerification.http_url
|
record.txt_value
|
||||||
) {
|
) {
|
||||||
acc.push({
|
return {
|
||||||
type: 'http',
|
validationType: 'ssl' as const,
|
||||||
body: ownershipVerification.http_body,
|
type: 'txt' as const,
|
||||||
url: ownershipVerification.http_url,
|
status: response.result[0].ssl.status ?? 'pending',
|
||||||
});
|
key: record.txt_name,
|
||||||
}
|
value: record.txt_value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
'type' in ownershipVerification &&
|
'type' in record &&
|
||||||
ownershipVerification.type === 'txt' &&
|
record.type === 'txt' &&
|
||||||
ownershipVerification.value &&
|
record.value &&
|
||||||
ownershipVerification.name
|
record.name
|
||||||
) {
|
) {
|
||||||
acc.push({
|
return {
|
||||||
type: 'txt',
|
validationType: 'ownership' as const,
|
||||||
value: ownershipVerification.value,
|
type: 'txt' as const,
|
||||||
name: ownershipVerification.name,
|
status: response.result[0].status ?? 'pending',
|
||||||
});
|
key: record.name,
|
||||||
}
|
value: record.value,
|
||||||
|
};
|
||||||
return acc;
|
}
|
||||||
},
|
},
|
||||||
[] as CustomHostnameDetails['ownershipVerifications'],
|
)
|
||||||
),
|
.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,
|
||||||
|
},
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Field, InputType } from '@nestjs/graphql';
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { IsOptional, IsBoolean, IsString } from 'class-validator';
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class GetAuthorizationUrlInput {
|
export class GetAuthorizationUrlInput {
|
||||||
|
|||||||
@ -222,7 +222,7 @@ export class WorkspaceResolver {
|
|||||||
|
|
||||||
@Query(() => CustomHostnameDetails, { nullable: true })
|
@Query(() => CustomHostnameDetails, { nullable: true })
|
||||||
@UseGuards(WorkspaceAuthGuard)
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
async getHostnameDetails(
|
async getCustomHostnameDetails(
|
||||||
@AuthWorkspace() { hostname }: Workspace,
|
@AuthWorkspace() { hostname }: Workspace,
|
||||||
): Promise<CustomHostnameDetails | undefined> {
|
): Promise<CustomHostnameDetails | undefined> {
|
||||||
if (!hostname) return undefined;
|
if (!hostname) return undefined;
|
||||||
|
|||||||
29
yarn.lock
29
yarn.lock
@ -17590,13 +17590,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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:*":
|
"@types/range-parser@npm:*":
|
||||||
version: 1.2.7
|
version: 1.2.7
|
||||||
resolution: "@types/range-parser@npm:1.2.7"
|
resolution: "@types/range-parser@npm:1.2.7"
|
||||||
@ -23122,21 +23115,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"cloudflare@npm:^3.5.0":
|
"cloudflare@npm:^4.0.0":
|
||||||
version: 3.5.0
|
version: 4.0.0
|
||||||
resolution: "cloudflare@npm:3.5.0"
|
resolution: "cloudflare@npm:4.0.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node": "npm:^18.11.18"
|
"@types/node": "npm:^18.11.18"
|
||||||
"@types/node-fetch": "npm:^2.6.4"
|
"@types/node-fetch": "npm:^2.6.4"
|
||||||
"@types/qs": "npm:^6.9.7"
|
|
||||||
abort-controller: "npm:^3.0.0"
|
abort-controller: "npm:^3.0.0"
|
||||||
agentkeepalive: "npm:^4.2.1"
|
agentkeepalive: "npm:^4.2.1"
|
||||||
form-data-encoder: "npm:1.7.2"
|
form-data-encoder: "npm:1.7.2"
|
||||||
formdata-node: "npm:^4.3.2"
|
formdata-node: "npm:^4.3.2"
|
||||||
node-fetch: "npm:^2.6.7"
|
node-fetch: "npm:^2.6.7"
|
||||||
qs: "npm:^6.10.3"
|
checksum: 10c0/d83a4d0544de5794935acb802c875c6f718713d8c7613b2a74d6145b922f18415e514cc57bbef9249726d83c0788ac8a72517a69efb1fd5b5af58b654403c375
|
||||||
web-streams-polyfill: "npm:^3.2.1"
|
|
||||||
checksum: 10c0/bb48ff68a4f5b7e945ceec570e7e17251ad167d8d2dadf8099fd74df46582356382b8cd3f62a9da74cd3a208f8f5181a40c561bc5873592d6a4930458c0e7b86
|
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -40591,15 +40581,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"qs@npm:~6.5.2":
|
||||||
version: 6.5.3
|
version: 6.5.3
|
||||||
resolution: "qs@npm:6.5.3"
|
resolution: "qs@npm:6.5.3"
|
||||||
@ -45936,7 +45917,7 @@ __metadata:
|
|||||||
cache-manager: "npm:^5.4.0"
|
cache-manager: "npm:^5.4.0"
|
||||||
cache-manager-redis-yet: "npm:^4.1.2"
|
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: "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"
|
connect-redis: "npm:^7.1.1"
|
||||||
express-session: "npm:^1.18.1"
|
express-session: "npm:^1.18.1"
|
||||||
graphql-middleware: "npm:^6.1.35"
|
graphql-middleware: "npm:^6.1.35"
|
||||||
|
|||||||
Reference in New Issue
Block a user