From 49006c4bea1de8fa90e2552d09550fb93136442d Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 29 Jun 2025 16:08:18 +0300 Subject: [PATCH 1/8] tmp From 6e919e36f929f863c73d6084a2074d57426e1d68 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 29 Jun 2025 16:49:41 +0300 Subject: [PATCH 2/8] Create certificate when add domain --- .../builder/features/topbar/add-domain.tsx | 1 + .../app/builder/features/topbar/domains.tsx | 2 ++ packages/domain/src/db/domain.ts | 24 ++++++++----- packages/domain/src/trpc/domain.ts | 2 ++ packages/trpc-interface/src/shared/domain.ts | 34 ++++++++++++++----- 5 files changed, 45 insertions(+), 18 deletions(-) diff --git a/apps/builder/app/builder/features/topbar/add-domain.tsx b/apps/builder/app/builder/features/topbar/add-domain.tsx index 5b928280454e..515cbe9317e8 100644 --- a/apps/builder/app/builder/features/topbar/add-domain.tsx +++ b/apps/builder/app/builder/features/topbar/add-domain.tsx @@ -63,6 +63,7 @@ export const AddDomain = ({ domain, projectId, }); + console.info(result); if (result.success === false) { toast.error(result.error); diff --git a/apps/builder/app/builder/features/topbar/domains.tsx b/apps/builder/app/builder/features/topbar/domains.tsx index b94b548c4aad..0ec55a9dcc73 100644 --- a/apps/builder/app/builder/features/topbar/domains.tsx +++ b/apps/builder/app/builder/features/topbar/domains.tsx @@ -220,6 +220,7 @@ const DomainItem = ({ setVerifyError(undefined); setIsCheckStateInProgress(true); + /* const verifyResult = await nativeClient.domain.verify.mutate({ projectId: projectDomain.projectId, domainId: projectDomain.domainId, @@ -229,6 +230,7 @@ const DomainItem = ({ setVerifyError(verifyResult.error); return; } + */ await refresh(); }); diff --git a/packages/domain/src/db/domain.ts b/packages/domain/src/db/domain.ts index d9e4081b017a..d94b476e7c64 100644 --- a/packages/domain/src/db/domain.ts +++ b/packages/domain/src/db/domain.ts @@ -9,7 +9,14 @@ import { cnameFromUserId } from "./cname-from-user-id"; import type { Project } from "@webstudio-is/project"; import type { Database } from "@webstudio-is/postrest/index.server"; -type Result = { success: false; error: string } | { success: true }; +type DomainValidation = { + txtName: string; + txtValue: string; +}; + +type Result = + | { success: false; error: string } + | { success: true; data: { validation: DomainValidation[] } }; /** * Creates 2 entries in the database: @@ -54,7 +61,6 @@ export const create = async ( } const validationResult = validateDomain(props.domain); - if (validationResult.success === false) { return validationResult; } @@ -74,7 +80,6 @@ export const create = async ( { onConflict: "domain", ignoreDuplicates: true } ) .eq("domain", domain); - if (upsertResult.error) { return { success: false, error: upsertResult.error.message }; } @@ -85,7 +90,6 @@ export const create = async ( .select("id") .eq("domain", domain) .single(); - if (domainRow.error) { return { success: false, error: domainRow.error.message }; } @@ -99,12 +103,14 @@ export const create = async ( txtRecord, cname: await cnameFromUserId(ownerId), }); - if (result.error) { return { success: false, error: result.error.message }; } - return { success: true }; + // @todo: TXT verification and domain initialization should be implemented in the future as queue service + return await context.domain.domainTrpc.create.mutate({ + domain, + }); }; /** @@ -153,7 +159,6 @@ export const verify = async ( // @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) { @@ -172,7 +177,7 @@ export const verify = async ( return { success: false, error: domainUpdateResult.error.message }; } - return { success: true }; + return { success: true, data: createDomainResult.data }; }; /** @@ -205,7 +210,7 @@ export const remove = async ( return { success: false, error: deleteResult.error.message }; } - return { success: true }; + return { success: true, data: { validation: [] } }; }; type Status = "active" | "pending" | "error"; @@ -254,6 +259,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..ba26bc04ec64 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", [ @@ -172,6 +173,7 @@ export const domainRouter = router({ } as const; } }), + /** * Update *.wstd.* domain */ diff --git a/packages/trpc-interface/src/shared/domain.ts b/packages/trpc-interface/src/shared/domain.ts index 9951eb29c80d..9c7cfc7999fd 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({ + validation: z.array( + z.object({ txtName: z.string(), txtValue: z.string() }) + ), + }) + ) + ) .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: { validation: [] } }; }), 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", [ From ab32e6a6132eaf41cc34dbfd0b429fec57746bde Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 29 Jun 2025 17:26:12 +0300 Subject: [PATCH 3/8] Use verification instead of validation --- packages/domain/src/db/domain.ts | 10 +++++----- packages/trpc-interface/src/shared/domain.ts | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/domain/src/db/domain.ts b/packages/domain/src/db/domain.ts index d94b476e7c64..daae2dc3ee32 100644 --- a/packages/domain/src/db/domain.ts +++ b/packages/domain/src/db/domain.ts @@ -9,14 +9,14 @@ import { cnameFromUserId } from "./cname-from-user-id"; import type { Project } from "@webstudio-is/project"; import type { Database } from "@webstudio-is/postrest/index.server"; -type DomainValidation = { - txtName: string; - txtValue: string; +type Verification = { + name: string; + value: string; }; type Result = | { success: false; error: string } - | { success: true; data: { validation: DomainValidation[] } }; + | { success: true; data: { verification?: Verification } }; /** * Creates 2 entries in the database: @@ -210,7 +210,7 @@ export const remove = async ( return { success: false, error: deleteResult.error.message }; } - return { success: true, data: { validation: [] } }; + return { success: true, data: {} }; }; type Status = "active" | "pending" | "error"; diff --git a/packages/trpc-interface/src/shared/domain.ts b/packages/trpc-interface/src/shared/domain.ts index 9c7cfc7999fd..73bf84ac56cd 100644 --- a/packages/trpc-interface/src/shared/domain.ts +++ b/packages/trpc-interface/src/shared/domain.ts @@ -44,9 +44,9 @@ export const domainRouter = router({ .output( createOutput( z.object({ - validation: z.array( - z.object({ txtName: z.string(), txtValue: z.string() }) - ), + verification: z + .object({ name: z.string(), value: z.string() }) + .optional(), }) ) ) @@ -66,7 +66,7 @@ export const domainRouter = router({ domainStates.set(input.domain, "pending"); - return { success: true, data: { validation: [] } }; + return { success: true, data: {} }; }), refresh: procedure From 3ffd2f449c5ed6622a03217435ffca0c7dd11f41 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 29 Jun 2025 17:57:16 +0300 Subject: [PATCH 4/8] Add cloudflare verification txt --- .../builder/features/topbar/add-domain.tsx | 2 -- .../app/builder/features/topbar/domains.tsx | 27 +++++++++++----- .../app/builder/features/topbar/entri.tsx | 4 +-- packages/domain/src/db/domain.ts | 32 +++++++++---------- 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/apps/builder/app/builder/features/topbar/add-domain.tsx b/apps/builder/app/builder/features/topbar/add-domain.tsx index 515cbe9317e8..775ec6c5d691 100644 --- a/apps/builder/app/builder/features/topbar/add-domain.tsx +++ b/apps/builder/app/builder/features/topbar/add-domain.tsx @@ -63,8 +63,6 @@ export const AddDomain = ({ domain, projectId, }); - console.info(result); - if (result.success === false) { toast.error(result.error); setError(result.error); diff --git a/apps/builder/app/builder/features/topbar/domains.tsx b/apps/builder/app/builder/features/topbar/domains.tsx index 0ec55a9dcc73..a2f4effd15fe 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]; @@ -290,20 +291,30 @@ const DomainItem = ({ const publisherHost = useStore($publisherHost); const cname = extractCname(projectDomain.domain); - const dnsRecords = [ + let verification: undefined | { name: string; value: string }; + try { + verification = 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, - { + }, + ]; + if (verification) { + dnsRecords.push({ type: "TXT", - host: cname === "@" ? "_webstudio_is" : `_webstudio_is.${cname}`, - value: projectDomain.expectedTxtRecord, + host: verification.name, + value: verification.value, ttl: 300, - } as const, - ]; + }); + } return ( Date: Sun, 29 Jun 2025 19:24:32 +0300 Subject: [PATCH 5/8] Fix check status --- .../app/builder/features/topbar/domains.tsx | 2 - packages/domain/src/db/domain.ts | 38 +------------------ packages/domain/src/trpc/domain.ts | 2 + 3 files changed, 3 insertions(+), 39 deletions(-) diff --git a/apps/builder/app/builder/features/topbar/domains.tsx b/apps/builder/app/builder/features/topbar/domains.tsx index a2f4effd15fe..5e9f40978c85 100644 --- a/apps/builder/app/builder/features/topbar/domains.tsx +++ b/apps/builder/app/builder/features/topbar/domains.tsx @@ -221,7 +221,6 @@ const DomainItem = ({ setVerifyError(undefined); setIsCheckStateInProgress(true); - /* const verifyResult = await nativeClient.domain.verify.mutate({ projectId: projectDomain.projectId, domainId: projectDomain.domainId, @@ -231,7 +230,6 @@ const DomainItem = ({ setVerifyError(verifyResult.error); return; } - */ await refresh(); }); diff --git a/packages/domain/src/db/domain.ts b/packages/domain/src/db/domain.ts index 291be7ea6bf0..af1031c07482 100644 --- a/packages/domain/src/db/domain.ts +++ b/packages/domain/src/db/domain.ts @@ -133,46 +133,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, - }); - - 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 }; } diff --git a/packages/domain/src/trpc/domain.ts b/packages/domain/src/trpc/domain.ts index ba26bc04ec64..fece10ebf3f4 100644 --- a/packages/domain/src/trpc/domain.ts +++ b/packages/domain/src/trpc/domain.ts @@ -260,6 +260,7 @@ export const domainRouter = router({ } as const; } }), + remove: procedure .input( z.object({ @@ -283,6 +284,7 @@ export const domainRouter = router({ } as const; } }), + countTotalDomains: procedure.query(async ({ ctx }) => { try { if ( From 40c4d69ace979355bd0741bd153ede10106a73a3 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 29 Jun 2025 19:48:10 +0300 Subject: [PATCH 6/8] Remove verified --- .../features/topbar/domain-checkbox.tsx | 5 +- .../app/builder/features/topbar/domains.tsx | 75 +++---------------- .../app/builder/features/topbar/publish.tsx | 4 +- apps/builder/app/shared/db/canvas.server.ts | 4 +- packages/domain/src/trpc/domain.ts | 6 +- 5 files changed, 16 insertions(+), 78 deletions(-) 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 5e9f40978c85..ad2408446814 100644 --- a/apps/builder/app/builder/features/topbar/domains.tsx +++ b/apps/builder/app/builder/features/topbar/domains.tsx @@ -51,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; @@ -100,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; @@ -183,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 || @@ -215,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(); }); @@ -268,13 +251,6 @@ const DomainItem = ({ return; } - if (status === "UNVERIFIED") { - startTransition(async () => { - await handleVerify(); - await handleUpdateStatus(); - }); - return; - } startTransition(async () => { await handleUpdateStatus(); }); @@ -354,7 +330,7 @@ const DomainItem = ({ } > - {status === "UNVERIFIED" && ( + {status === "VERIFIED_INITIALIZING" && ( <>