Skip to content

Commit

Permalink
feat: get rid of vercel domains API, just check cname instead
Browse files Browse the repository at this point in the history
  • Loading branch information
vklimontovich committed Dec 20, 2023
1 parent 4e85d75 commit 6d2beaf
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 353 deletions.
11 changes: 0 additions & 11 deletions webapps/console/lib/ee-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { get } from "./useApi";
import { DomainStatus } from "./server/ee";
import * as auth from "firebase/auth";

export type ClassicProjectStatus = {
Expand All @@ -11,7 +10,6 @@ export type ClassicProjectStatus = {
};

export interface EeClient {
attachDomain(domain: string): Promise<DomainStatus>;
checkClassicProject(): Promise<ClassicProjectStatus>;
createCustomToken(): Promise<string>;
}
Expand Down Expand Up @@ -39,15 +37,6 @@ export function getEeClient(host: string, workspaceId: string): EeClient {
return cachedToken;
};
return {
attachDomain: async domain => {
cachedToken = await refreshTokenIfNeeded();
return await get(removeDoubleSlashes(`${host}/api/domain`), {
query: { domain },
headers: {
Authorization: `Bearer ${cachedToken.token}`,
},
});
},
checkClassicProject: async () => {
const fbToken = await auth.getAuth().currentUser?.getIdToken();
return await get(removeDoubleSlashes(`${host}/api/is-active`), {
Expand Down
39 changes: 39 additions & 0 deletions webapps/console/lib/server/custom-domains.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { db } from "./db";
import { StreamConfig } from "../schema";
import dns from "dns";
import { getLog } from "juava";

type DomainAvailability = { available: true; usedInWorkspaces?: never } | { available: false; usedInWorkspace: string };

export const customDomainCnames = process.env.CUSTOM_DOMAIN_CNAMES?.split(",");

/**
* Tells if the given domain is used in other workspaces.
*/
Expand All @@ -26,3 +30,38 @@ export async function isDomainAvailable(domain: string, workspaceId: string): Pr
return { available: true };
}
}

function resolveCname(domain: string): Promise<string | undefined> {
return new Promise((resolve, reject) => {
dns.resolveCname(domain, (err, addresses) => {
if (err) {
reject(err);
} else {
if (addresses.length === 1) {
resolve(addresses[0]);
} else if (!addresses || addresses.length === 0) {
resolve(undefined);
} else {
getLog()
.atWarn()
.log(`Domain ${domain} has multiple CNAME records: ${addresses.join(", ")}. Using first one`);
resolve(addresses[0]);
}
}
});
});
}

export async function isCnameValid(domain: string): Promise<boolean> {
if (!customDomainCnames || customDomainCnames.length == 0) {
throw new Error(`CUSTOM_DOMAIN_CNAMES is not set. isCnameValid() should not be called`);
}
let cnameRecord: string | undefined;
try {
cnameRecord = await resolveCname(domain);
} catch (e) {
getLog().atError().withCause(e).log(`Domain ${domain} has no CNAME records`);
return false;
}
return !!(cnameRecord && customDomainCnames.includes(cnameRecord.toLowerCase()));
}
10 changes: 0 additions & 10 deletions webapps/console/lib/server/ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,3 @@ export function createJwt(
const token = jwt.sign({ userId, email, workspaceId, exp: expiresSecondsTimestamp }, jwtSecret);
return { jwt: token, expiresAt: new Date(expiresSecondsTimestamp * 1000).toISOString() };
}

export type DomainStatus = { error?: string } & (
| { needsConfiguration: false }
| { needsConfiguration: true; configurationType: "cname"; cnameValue: string }
| {
needsConfiguration: true;
configurationType: "verification";
verification: { type: string; domain: string; value: string }[];
}
);
21 changes: 21 additions & 0 deletions webapps/console/lib/shared/domain-check-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { z } from "zod";
import { Simplify } from "type-fest";

export const DomainCheckResponse = z.union([
z.object({
ok: z.literal(true),
reason: z.never().optional(),
}),
z.object({
ok: z.literal(false),
reason: z.union([z.literal("used_by_other_workspace"), z.literal("invalid_domain_name")]),
cnameValue: z.never().optional(),
}),
z.object({
ok: z.literal(false),
reason: z.literal("requires_cname_configuration"),
cnameValue: z.string().optional(),
}),
]);

export type DomainCheckResponse = Simplify<z.infer<typeof DomainCheckResponse>>;
63 changes: 33 additions & 30 deletions webapps/console/pages/[workspaceId]/streams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { getEeClient } from "../../lib/ee-client";
import { assertDefined, requireDefined } from "juava";
import { ReloadOutlined } from "@ant-design/icons";
import { confirmOp, feedbackError } from "../../lib/ui";
import type { DomainStatus } from "../../lib/server/ee";
import { getAntdModal, useAntdModal } from "../../lib/modal";
import { get } from "../../lib/useApi";
import { Activity, AlertTriangle, Check, Globe, Wrench, Zap } from "lucide-react";
Expand All @@ -27,6 +26,7 @@ import { useLinksQuery } from "../../lib/queries";
import { toURL } from "../../lib/shared/url";
import JSON5 from "json5";
import { EditorToolbar } from "../../components/EditorToolbar/EditorToolbar";
import { DomainCheckResponse } from "../../lib/shared/domain-check-response";

const Streams: React.FC<any> = () => {
return (
Expand Down Expand Up @@ -77,10 +77,10 @@ const CustomDomain: React.FC<{ domain: string; deleteDomain: () => Promise<void>
);
const [reloadTrigger, setReloadTrigger] = useState(0);
const [deleting, setDeleting] = useState(false);
const { data, isLoading, error, refetch } = useQuery<DomainStatus>(
const { data, isLoading, error, refetch } = useQuery<DomainCheckResponse>(
["domain-status", domain.toLowerCase(), reloadTrigger],
async () => {
return await eeClient.attachDomain(domain);
return await get(`/api/${workspace.id}/domain-check?domain=${domain.toLowerCase()}`);
},
{ cacheTime: 0 }
);
Expand All @@ -94,9 +94,7 @@ const CustomDomain: React.FC<{ domain: string; deleteDomain: () => Promise<void>
{/*</div>*/}
<div className={"text-blue-600 w-4 h-4 mr-1.5"}>
<Globe
className={`w-full h-full ${
error || data?.error ? "text-red-600" : data?.needsConfiguration ? "text-yellow-600" : "text-blue-600"
}`}
className={`w-full h-full ${error ? "text-red-600" : data?.ok ? "text-blue-600" : "text-yellow-600"}`}
/>
</div>
<div className="font-bold text-lg">{domain}</div>
Expand All @@ -113,14 +111,14 @@ const CustomDomain: React.FC<{ domain: string; deleteDomain: () => Promise<void>
<FaExternalLinkAlt />
</Button>
</Tooltip>
{data?.needsConfiguration && (
{!data?.ok && (
<Tooltip title="See configuration instructions">
<Button
type="text"
danger
disabled={isLoading || deleting}
onClick={() => {
DomainConfigurationInstructions.show({ domain, status: data });
DomainConfigurationInstructions.show({ domain, status: data! });
}}
className="border-0"
>
Expand Down Expand Up @@ -174,29 +172,29 @@ const CustomDomain: React.FC<{ domain: string; deleteDomain: () => Promise<void>
</span>
</StatusBadge>
);
} else if (error || data?.error) {
} else if (error) {
return <StatusBadge status="error">ERROR</StatusBadge>;
} else if (data?.needsConfiguration) {
} else if (!data?.ok) {
return <StatusBadge status="warning">Configuration Required</StatusBadge>;
} else {
return <StatusBadge status="success">OK</StatusBadge>;
}
})()}
</div>
{(error || data?.error) && (
{error && (
<div className="flex items-start mt-1">
<div className={"mr-2"}>Description:</div>
<div className="">{`${data?.error || "Internal error"}`}</div>
<div className="">{`${"Internal error"}`}</div>
</div>
)}
{data?.needsConfiguration && (
{!data?.ok && (
<div className="flex items-start mt-1">
<div className={"mr-2"}>Description:</div>
<div className="">
See{" "}
<a
className={"cursor-pointer"}
onClick={() => DomainConfigurationInstructions.show({ domain, status: data })}
onClick={() => DomainConfigurationInstructions.show({ domain, status: data! })}
>
<u>configuration instructions</u>
</a>
Expand Down Expand Up @@ -234,25 +232,16 @@ export const DNSRecordTable: React.FC<DNSRecordTableProps> = ({ records }) => {
);
};

export type DomainInstructionsProps = { domain: string; status: DomainStatus };
export type DomainInstructionsProps = { domain: string; status: DomainCheckResponse };
const DomainConfigurationInstructions: React.FC<DomainInstructionsProps> & {
show: (p: DomainInstructionsProps) => void;
} = ({ domain, status }) => {
if (status.needsConfiguration && status.configurationType === "cname") {
if (status.reason === "requires_cname_configuration") {
return (
<div>
<h3>Set the following record on your DNS provider to continue</h3>
<p className="bg-bgLight py-2 my-4">
<DNSRecordTable records={[{ type: "CNAME", domain, value: status.cnameValue }]} />
</p>
</div>
);
} else if (status.needsConfiguration && status.configurationType == "verification") {
return (
<div>
<h3>Set the following record on your DNS provider to continue</h3>
<p className="bg-bgLight py-2 my-4">
<DNSRecordTable records={status.verification} />
<DNSRecordTable records={[{ type: "CNAME", domain, value: status.cnameValue! }]} />
</p>
</div>
);
Expand Down Expand Up @@ -282,10 +271,24 @@ const DomainsEditor: React.FC<CustomWidgetProps<string[]>> = props => {
const add = async () => {
setAddPending(true);
try {
const { available } = await get(`/api/${workspace.id}/domain-check?domain=${addValue}`);
if (!available) {
feedbackError(`Domain ${addValue} is not available. It is used by other workspace`);
return;
const available: DomainCheckResponse = await get(`/api/${workspace.id}/domain-check?domain=${addValue}`);
if (!available.ok) {
if (available.reason === "used_by_other_workspace") {
feedbackError(
<>
Domain <code>{addValue}</code> is not available. It is used by other workspace. Contact{" "}
<code>[email protected]</code> if you think this is a mistake
</>
);
return;
} else if (available.reason === "invalid_domain_name") {
feedbackError(
<>
Invalid domain name <code>{addValue}</code>
</>
);
return;
}
}
const newVal = [...domains, addValue as string];
setDomains(newVal);
Expand Down
63 changes: 33 additions & 30 deletions webapps/console/pages/api/[workspaceId]/domain-check.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,41 @@
import { Api, inferUrl, nextJsApiHandler, verifyAccess } from "../../../lib/api";
import { getServerLog } from "../../../lib/server/log";

import { z } from "zod";
import { isDomainAvailable } from "../../../lib/server/custom-domains";
import { customDomainCnames, isCnameValid, isDomainAvailable } from "../../../lib/server/custom-domains";
import { DomainCheckResponse } from "../../../lib/shared/domain-check-response";
import { createRoute, verifyAccess } from "../../../lib/api";

const log = getServerLog("custom-domains");

export const api: Api = {
url: inferUrl(__filename),
GET: {
export default createRoute()
.GET({
auth: true,
types: {
query: z.object({
workspaceId: z.string(),
domain: z.string(),
}),
result: z.object({
available: z.boolean(),
}),
},
handle: async ({ user, query }) => {
await verifyAccess(user, query.workspaceId);
const domainAvailability = await isDomainAvailable(query.domain, query.workspaceId);
if (!domainAvailability.available) {
log
.atWarn()
.log(
`Domain '${query.domain}' can't be added to workspace ${query.workspaceId}. It is used by ${domainAvailability.usedInWorkspace}`
);
return { available: false };
}
return { available: true };
},
},
};
query: z.object({
workspaceId: z.string(),
domain: z.string(),
}),
result: DomainCheckResponse,
})
.handler(async ({ user, query: { workspaceId, domain } }) => {
if (!customDomainCnames || customDomainCnames.length == 0) {
throw new Error(`CUSTOM_DOMAIN_CNAMES is not set`);
}
await verifyAccess(user, workspaceId);
const domainAvailability = await isDomainAvailable(domain, workspaceId);
if (!domainAvailability.available) {
log
.atWarn()
.log(
`Domain '${domain}' can't be added to workspace ${workspaceId}. It is used by ${domainAvailability.usedInWorkspace}`
);
return { ok: false, reason: "used_by_other_workspace" };
}

export default nextJsApiHandler(api);
const cnameValid = await isCnameValid(domain);
if (!cnameValid) {
log.atWarn().log(`Domain ${domain} is not valid`);
return { ok: false, reason: "requires_cname_configuration", cnameValue: customDomainCnames[0] };
}
return { ok: true };
})
.toNextApiHandler();
Loading

2 comments on commit 6d2beaf

@vercel
Copy link

@vercel vercel bot commented on 6d2beaf Dec 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

new-jitsu – ./webapps/console

ag.ru
logu.au
ozon.ru
sse.ere
erxes.io
baidu.dom
ilmiya.io
sambla.se
bobsec.com
sambla.com
agro4u.life
bluetick.ai
myilmiya.io
protontv.eu
t.quenti.io
alicesec.com
d.askloan.tw
dev.aclis.io
docs.dh19.de
docs.dh19.eu
hunterbi.com
joseviso.com
mydomain.dom
https.bluetick.ai
ji.degulesider.dk
jitsu.ivve.health
metabase.erxes.io
t.clickncruise.hu
test.d2.jitsu.com
cloud.yupaopao.com
data.investing.com
data.mycompany.com
data.usepolygon.io
demosite.jitsu.com
dev.driverdeck.app
n8n.paziresh24.com
new.enterticket.es
t-dev.papermark.io
test2.d2.jitsu.com
uniquecafes.com.br
www.sidetrekai.com
colectha.voolu.shop
crm.myguestcare.com
data.sidetrekai.com
data.timeplus.cloud
localhost.jitsu.com
report.improvado.io
trk.myguestcare.com
www.sevenbillion.co
analytics.mtrsvc.com
data.embeddables.com
dataqa.investing.com
dev.blazingboost.com
j.israeladvocate.org
mercury.stagehub.com
store.sidetrekai.com
teslahenry.github.io
data.hogarlylabs.tech
data.your-company.com
event.clickncruise.hu
event.clickncruise.ro
test-domain.jitsu.com
test.bigfootproof.com
teste.fazcomex.com.br
analytics.dev.knekt.io
loraboutiquedental.com
notion.twelftree.co.uk
dev-portal.zoopsign.com
event.tradejobsnz.co.nz
investing-poc.jitsu.dev
savvy-replay.jitsu.tech
data.analytics-smart.com
data.handelsregister.app
event.clickncruise.co.uk
jt.fairhopeweb.github.io
savvy-replay2.jitsu.tech
savvy-replay3.jitsu.tech
savvy-replay4.jitsu.tech
track.alquimiaweb.com.br
track.pressance-group.jp
track.uniquecafes.com.br
colectha.agenciavoolu.com
kolectha.agenciavoolu.com
lp.loraboutiquedental.com
stage-portal.zoopsign.com
new-jitsu-jitsu.vercel.app
lodercom-colectha.voolu.shop
warehouse1.trendstyle.com.au
d0.livingdesignsfurniture.com
ingest-load-testing.jitsu.dev
jitsu.precisaosistemas.com.br
analytics.inspiresolutions.app
betteruptime-monitoring.jitsu.dev
canvas.livingdesignsfurniture.com
analytics.dev.inspiresolutions.app
cl9vt45z50001znkunc6v8fmm.d.jitsu.com
clm2jikrm00002v6r5l6niws3.d.jitsu.com
new-jitsu-git-newjitsu-jitsu.vercel.app
3000-rajaraodv-customerdemo-nmpsqwflswt.ws-us102.gitpod.io
new.jitsu.dev

@vercel
Copy link

@vercel vercel bot commented on 6d2beaf Dec 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

new-jitsu-ee-api – ./webapps/ee-api

onetag-ee-api.vercel.app
new-jitsu-ee-api-jitsu.vercel.app
new-jitsu-ee-api-git-newjitsu-jitsu.vercel.app
ee.jitsu.dev

Please sign in to comment.