diff --git a/apps/builder/app/builder/features/topbar/add-domain.tsx b/apps/builder/app/builder/features/topbar/add-domain.tsx
index 5b928280454e..775ec6c5d691 100644
--- a/apps/builder/app/builder/features/topbar/add-domain.tsx
+++ b/apps/builder/app/builder/features/topbar/add-domain.tsx
@@ -63,7 +63,6 @@ export const AddDomain = ({
domain,
projectId,
});
-
if (result.success === false) {
toast.error(result.error);
setError(result.error);
diff --git a/apps/builder/app/builder/features/topbar/domain-checkbox.tsx b/apps/builder/app/builder/features/topbar/domain-checkbox.tsx
index 849a3ff339f8..a5af32699e0e 100644
--- a/apps/builder/app/builder/features/topbar/domain-checkbox.tsx
+++ b/apps/builder/app/builder/features/topbar/domain-checkbox.tsx
@@ -59,9 +59,8 @@ export const DomainCheckbox = (props: DomainCheckboxProps) => {
const disabled = hasProPlan ? props.disabled : true;
const hideDomainCheckbox =
- project.domainsVirtual.filter(
- (domain) => domain.status === "ACTIVE" && domain.verified
- ).length === 0 && hasProPlan;
+ project.domainsVirtual.filter((domain) => domain.status === "ACTIVE")
+ .length === 0 && hasProPlan;
return (
diff --git a/apps/builder/app/builder/features/topbar/domains.tsx b/apps/builder/app/builder/features/topbar/domains.tsx
index b94b548c4aad..54dfe3a4d8ef 100644
--- a/apps/builder/app/builder/features/topbar/domains.tsx
+++ b/apps/builder/app/builder/features/topbar/domains.tsx
@@ -29,7 +29,7 @@ import {
useState,
type ReactNode,
} from "react";
-import { Entri } from "./entri";
+import { Entri, type DnsRecord } from "./entri";
import { nativeClient } from "~/shared/trpc/trpc-client";
import { useStore } from "@nanostores/react";
import { $publisherHost } from "~/shared/nano-states";
@@ -38,6 +38,7 @@ import { useEffectEvent } from "~/shared/hook-utils/effect-event";
import { DomainCheckbox } from "./domain-checkbox";
import { CopyToClipboard } from "~/builder/shared/copy-to-clipboard";
import { RelativeTime } from "~/builder/shared/relative-time";
+import { z } from "zod";
export type Domain = Project["domainsVirtual"][number];
@@ -50,9 +51,7 @@ const InputEllipsis = styled(InputField, {
});
export const getStatus = (projectDomain: Domain) =>
- projectDomain.verified
- ? (`VERIFIED_${projectDomain.status}` as const)
- : `UNVERIFIED`;
+ `VERIFIED_${projectDomain.status}` as const;
export const PENDING_TIMEOUT =
process.env.NODE_ENV === "production" ? 60 * 3 * 1000 : 35000;
@@ -99,10 +98,6 @@ const getStatusText = (props: {
let text: ReactNode = "Something went wrong";
switch (status) {
- case "UNVERIFIED":
- text = "Status: Not verified";
- break;
-
case "VERIFIED_INITIALIZING":
text = "Status: Initializing CNAME";
break;
@@ -182,9 +177,8 @@ const DomainItem = ({
const DAY_IN_MS = 24 * 60 * 60 * 1000;
- const status = projectDomain.verified
- ? (`VERIFIED_${projectDomain.status}` as `VERIFIED_${DomainStatus}`)
- : `UNVERIFIED`;
+ const status =
+ `VERIFIED_${projectDomain.status}` as `VERIFIED_${DomainStatus}`;
const [isStatusLoading, setIsStatusLoading] = useState(
initiallyOpen ||
@@ -214,22 +208,12 @@ const DomainItem = ({
await refresh();
};
- const [verifyError, setVerifyError] = useState(undefined);
-
const handleVerify = useEffectEvent(async () => {
- setVerifyError(undefined);
setIsCheckStateInProgress(true);
-
- const verifyResult = await nativeClient.domain.verify.mutate({
+ await nativeClient.domain.verify.mutate({
projectId: projectDomain.projectId,
domainId: projectDomain.domainId,
});
-
- if (verifyResult.success === false) {
- setVerifyError(verifyResult.error);
- return;
- }
-
await refresh();
});
@@ -267,17 +251,10 @@ const DomainItem = ({
return;
}
- if (status === "UNVERIFIED") {
- startTransition(async () => {
- await handleVerify();
- await handleUpdateStatus();
- });
- return;
- }
startTransition(async () => {
await handleUpdateStatus();
});
- }, [status, handleVerify, handleUpdateStatus, isStatusLoading]);
+ }, [status, handleUpdateStatus, isStatusLoading]);
const domainStatus = getStatus(projectDomain);
@@ -288,20 +265,30 @@ const DomainItem = ({
const publisherHost = useStore($publisherHost);
const cname = extractCname(projectDomain.domain);
- const dnsRecords = [
+ let verifications;
+ try {
+ verifications = z
+ .array(z.object({ name: z.string(), value: z.string() }))
+ .parse(JSON.parse(projectDomain.expectedTxtRecord));
+ } catch {
+ // empty block
+ }
+ const dnsRecords: DnsRecord[] = [
{
type: "CNAME",
host: cname,
value: `${projectDomain.cname}.customers.${publisherHost}`,
ttl: 300,
- } as const,
- {
+ },
+ ];
+ for (const { name, value } of verifications ?? []) {
+ dnsRecords.push({
type: "TXT",
- host: cname === "@" ? "_webstudio_is" : `_webstudio_is.${cname}`,
- value: projectDomain.expectedTxtRecord,
+ host: name,
+ value,
ttl: 300,
- } as const,
- ];
+ });
+ }
return (
}
>
- {status === "UNVERIFIED" && (
+ {status === "VERIFIED_INITIALIZING" && (
<>
)}
- {status !== "UNVERIFIED" && (
+ {status !== "VERIFIED_INITIALIZING" && (
<>
{updateStatusError && (
{updateStatusError}
@@ -383,33 +370,9 @@ const DomainItem = ({
- {status === "UNVERIFIED" && (
- <>
- {verifyError ? (
-
- Status: Failed to verify
-
- {verifyError}
-
- ) : (
- <>
- Status: Not verified
-
- Verification may take up to 24 hours but usually takes only
- a few minutes.
-
- >
- )}
- >
- )}
-
- {status !== "UNVERIFIED" && (
- <>
-
- {text}
-
- >
- )}
+
+ {text}
+
@@ -480,15 +443,6 @@ const DomainItem = ({
dnsRecords={dnsRecords}
domain={projectDomain.domain}
onClose={() => {
- // Sometimes Entri modal dialog hangs even if it's successful,
- // until they fix that, we'll just refresh the status here on every onClose event
- if (status === "UNVERIFIED") {
- startTransition(async () => {
- await handleVerify();
- await handleUpdateStatus();
- });
- return;
- }
startTransition(async () => {
await handleUpdateStatus();
});
diff --git a/apps/builder/app/builder/features/topbar/entri.tsx b/apps/builder/app/builder/features/topbar/entri.tsx
index 39af866ed889..c66129dd3c48 100644
--- a/apps/builder/app/builder/features/topbar/entri.tsx
+++ b/apps/builder/app/builder/features/topbar/entri.tsx
@@ -15,8 +15,8 @@ import { extractCname } from "./cname";
import { UploadIcon } from "@webstudio-is/icons";
// https://developers.entri.com/docs/install
-type DnsRecord = {
- type: "CNAME" | "ALIAS" | "TXT";
+export type DnsRecord = {
+ type: "CNAME" | "TXT";
host: string;
value: string;
ttl: number;
diff --git a/apps/builder/app/builder/features/topbar/publish.tsx b/apps/builder/app/builder/features/topbar/publish.tsx
index 1b8debca6db2..604472da8bdd 100644
--- a/apps/builder/app/builder/features/topbar/publish.tsx
+++ b/apps/builder/app/builder/features/topbar/publish.tsx
@@ -446,7 +446,7 @@ const Publish = ({
: [
project.domain,
...project.domainsVirtual
- .filter((domain) => domain.verified && domain.status === "ACTIVE")
+ .filter((domain) => domain.status === "ACTIVE")
.map((domain) => domain.domain),
];
@@ -734,7 +734,7 @@ const useCanAddDomain = () => {
const project = useStore($project);
const activeDomainsCount = project?.domainsVirtual.filter(
- (domain) => domain.status === "ACTIVE" && domain.verified
+ (domain) => domain.status === "ACTIVE"
).length;
useEffect(() => {
diff --git a/apps/builder/app/shared/db/canvas.server.ts b/apps/builder/app/shared/db/canvas.server.ts
index 8345c1563c69..0847697f47f2 100644
--- a/apps/builder/app/shared/db/canvas.server.ts
+++ b/apps/builder/app/shared/db/canvas.server.ts
@@ -38,9 +38,7 @@ export const loadProductionCanvasData = async (
project.domain === domain ||
currentProjectDomains.some(
(projectDomain) =>
- projectDomain.domain === domain &&
- projectDomain.status === "ACTIVE" &&
- projectDomain.verified
+ projectDomain.domain === domain && projectDomain.status === "ACTIVE"
)
);
}
diff --git a/packages/domain/src/db/domain.ts b/packages/domain/src/db/domain.ts
index d9e4081b017a..c0e007c1387d 100644
--- a/packages/domain/src/db/domain.ts
+++ b/packages/domain/src/db/domain.ts
@@ -54,7 +54,6 @@ export const create = async (
}
const validationResult = validateDomain(props.domain);
-
if (validationResult.success === false) {
return validationResult;
}
@@ -74,7 +73,6 @@ export const create = async (
{ onConflict: "domain", ignoreDuplicates: true }
)
.eq("domain", domain);
-
if (upsertResult.error) {
return { success: false, error: upsertResult.error.message };
}
@@ -85,21 +83,43 @@ export const create = async (
.select("id")
.eq("domain", domain)
.single();
-
if (domainRow.error) {
return { success: false, error: domainRow.error.message };
}
const domainId = domainRow.data.id;
- const txtRecord = crypto.randomUUID();
+
+ const verifications = [];
+ // @todo: TXT verification and domain initialization should be implemented in the future as queue service
+ const createDomainResult = await context.domain.domainTrpc.create.mutate({
+ domain,
+ });
+ if (createDomainResult.success === false) {
+ return createDomainResult;
+ }
+ if (createDomainResult.data.verification) {
+ verifications.push(createDomainResult.data.verification);
+ }
+
+ // create additionally root certificate for www subdomain
+ if (domain.startsWith("www.")) {
+ const createDomainResult = await context.domain.domainTrpc.create.mutate({
+ domain: domain.slice("www.".length),
+ });
+ if (createDomainResult.success === false) {
+ return createDomainResult;
+ }
+ if (createDomainResult.data.verification) {
+ verifications.push(createDomainResult.data.verification);
+ }
+ }
const result = await context.postgrest.client.from("ProjectDomain").insert({
domainId,
projectId: props.projectId,
- txtRecord,
+ txtRecord: JSON.stringify(verifications),
cname: await cnameFromUserId(ownerId),
});
-
if (result.error) {
return { success: false, error: result.error.message };
}
@@ -127,47 +147,10 @@ export const verify = async (
throw new Error("You don't have access to create this project domains");
}
- const projectDomain = await context.postgrest.client
- .from("ProjectDomain")
- .select(
- `
- txtRecord,
- cname,
- domain:Domain(*)
- `
- )
- .eq("domainId", props.domainId)
- .eq("projectId", props.projectId)
- .single();
-
- if (projectDomain.error) {
- return { success: false, error: projectDomain.error.message };
- }
-
- const domain = projectDomain.data.domain?.domain;
-
- if (domain == null) {
- return { success: false, error: "Domain not found" };
- }
-
- // @todo: TXT verification and domain initialization should be implemented in the future as queue service
- const createDomainResult = await context.domain.domainTrpc.create.mutate({
- domain,
- txtRecord: projectDomain.data.txtRecord,
- });
-
- if (createDomainResult.success === false) {
- return createDomainResult;
- }
-
const domainUpdateResult = await context.postgrest.client
.from("Domain")
- .update({
- status: "PENDING",
- txtRecord: projectDomain.data.txtRecord,
- })
+ .update({ status: "PENDING" })
.eq("id", props.domainId);
-
if (domainUpdateResult.error) {
return { success: false, error: domainUpdateResult.error.message };
}
@@ -254,6 +237,7 @@ export const updateStatus = async (
// @todo: must be implemented as workflow/queue service part of 3rd party domain initialization process
const statusResult = await context.domain.domainTrpc.getStatus.query({
domain,
+ method: "txt",
});
if (statusResult.success === false) {
diff --git a/packages/domain/src/trpc/domain.ts b/packages/domain/src/trpc/domain.ts
index 8f6bee07b122..ed95c69026dc 100644
--- a/packages/domain/src/trpc/domain.ts
+++ b/packages/domain/src/trpc/domain.ts
@@ -69,6 +69,7 @@ export const domainRouter = router({
} as const;
}
}),
+
publish: procedure
.input(
z.discriminatedUnion("destination", [
@@ -106,15 +107,13 @@ export const domainRouter = router({
currentProjectDomains.some(
(projectDomain) =>
projectDomain.domain === domain &&
- projectDomain.status === "ACTIVE" &&
- projectDomain.verified
+ projectDomain.status === "ACTIVE"
)
)
);
hasCustomDomain = currentProjectDomains.some(
- (projectDomain) =>
- projectDomain.status === "ACTIVE" && projectDomain.verified
+ (projectDomain) => projectDomain.status === "ACTIVE"
);
}
@@ -172,6 +171,7 @@ export const domainRouter = router({
} as const;
}
}),
+
/**
* Update *.wstd.* domain
*/
@@ -258,6 +258,7 @@ export const domainRouter = router({
} as const;
}
}),
+
remove: procedure
.input(
z.object({
@@ -281,6 +282,7 @@ export const domainRouter = router({
} as const;
}
}),
+
countTotalDomains: procedure.query(async ({ ctx }) => {
try {
if (
diff --git a/packages/trpc-interface/src/shared/domain.ts b/packages/trpc-interface/src/shared/domain.ts
index 9951eb29c80d..73bf84ac56cd 100644
--- a/packages/trpc-interface/src/shared/domain.ts
+++ b/packages/trpc-interface/src/shared/domain.ts
@@ -5,8 +5,7 @@
import { z } from "zod";
import { router, procedure } from "./trpc";
-const CreateInput = z.object({ domain: z.string(), txtRecord: z.string() });
-const Input = z.object({ domain: z.string() });
+const Method = z.union([z.literal("txt"), z.literal("http")]);
const createOutput = (data: T) =>
z.discriminatedUnion("success", [
@@ -33,11 +32,27 @@ export const domainRouter = router({
* Verify TXT record and add custom domain entry to DNS
*/
create: procedure
- .input(CreateInput)
- .output(createOutput(z.optional(z.undefined())))
+ .input(
+ z.object({
+ domain: z.string(),
+ /**
+ * when missing use txt validation method and instead create txt record
+ */
+ txtRecord: z.string().optional(),
+ })
+ )
+ .output(
+ createOutput(
+ z.object({
+ verification: z
+ .object({ name: z.string(), value: z.string() })
+ .optional(),
+ })
+ )
+ )
.mutation(async ({ input }) => {
const record = dnsTxtEntries.get(input.domain);
- if (record !== input.txtRecord) {
+ if (input.txtRecord && record !== input.txtRecord) {
// Return an error once then update the record
dnsTxtEntries.set(input.domain, input.txtRecord);
@@ -51,20 +66,21 @@ export const domainRouter = router({
domainStates.set(input.domain, "pending");
- return { success: true };
+ return { success: true, data: {} };
}),
refresh: procedure
- .input(Input)
- .output(createOutput(z.optional(z.undefined())))
+ .input(z.object({ domain: z.string(), method: Method.optional() }))
+ .output(createOutput(z.optional(z.unknown())))
.mutation(async () => {
return { success: true };
}),
+
/**
* Get status of verified domain
*/
getStatus: procedure
- .input(Input)
+ .input(z.object({ domain: z.string(), method: Method.optional() }))
.output(
createOutput(
z.discriminatedUnion("status", [