Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/builder/app/builder/features/topbar/add-domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export const AddDomain = ({
domain,
projectId,
});

if (result.success === false) {
toast.error(result.error);
setError(result.error);
Expand Down
5 changes: 2 additions & 3 deletions apps/builder/app/builder/features/topbar/domain-checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div style={{ display: hideDomainCheckbox ? "none" : "contents" }}>
Expand Down
104 changes: 29 additions & 75 deletions apps/builder/app/builder/features/topbar/domains.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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];

Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -214,22 +208,12 @@ const DomainItem = ({
await refresh();
};

const [verifyError, setVerifyError] = useState<string | undefined>(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();
});

Expand Down Expand Up @@ -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);

Expand All @@ -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 (
<CollapsibleDomainSection
Expand Down Expand Up @@ -343,7 +330,7 @@ const DomainItem = ({
</Grid>
}
>
{status === "UNVERIFIED" && (
{status === "VERIFIED_INITIALIZING" && (
<>
<Button
formAction={handleVerify}
Expand All @@ -356,7 +343,7 @@ const DomainItem = ({
</>
)}

{status !== "UNVERIFIED" && (
{status !== "VERIFIED_INITIALIZING" && (
<>
{updateStatusError && (
<Text color="destructive">{updateStatusError}</Text>
Expand All @@ -383,33 +370,9 @@ const DomainItem = ({

<Grid gap={2} css={{ mt: theme.spacing[5] }}>
<Grid gap={1}>
{status === "UNVERIFIED" && (
<>
{verifyError ? (
<Text color="destructive">
Status: Failed to verify
<br />
{verifyError}
</Text>
) : (
<>
<Text color="destructive">Status: Not verified</Text>
<Text color="subtle">
Verification may take up to 24 hours but usually takes only
a few minutes.
</Text>
</>
)}
</>
)}

{status !== "UNVERIFIED" && (
<>
<Text color={isVerifiedActive ? "success" : "destructive"}>
{text}
</Text>
</>
)}
<Text color={isVerifiedActive ? "success" : "destructive"}>
{text}
</Text>
</Grid>

<Text color="subtle">
Expand Down Expand Up @@ -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();
});
Expand Down
4 changes: 2 additions & 2 deletions apps/builder/app/builder/features/topbar/entri.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions apps/builder/app/builder/features/topbar/publish.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
];

Expand Down Expand Up @@ -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(() => {
Expand Down
4 changes: 1 addition & 3 deletions apps/builder/app/shared/db/canvas.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
);
}
Expand Down
72 changes: 28 additions & 44 deletions packages/domain/src/db/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export const create = async (
}

const validationResult = validateDomain(props.domain);

if (validationResult.success === false) {
return validationResult;
}
Expand All @@ -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 };
}
Expand All @@ -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 };
}
Expand Down Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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) {
Expand Down
Loading