Skip to content
Merged
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: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@types/d3": "^7.4.3",
"@uploadthing/react": "^7.3.0",
"@upstash/ratelimit": "^2.0.5",
"@vercel/sdk": "^1.7.1",
"ai": "^4.3.10",
"argon2": "^0.43.0",
"better-auth": "^1.2.7",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"use server";

import { z } from "zod";
import { authActionClient } from "@/actions/safe-action";
import { db } from "@comp/db";
import { revalidatePath, revalidateTag } from "next/cache";
import { Vercel } from "@vercel/sdk";
import { env } from "@/env.mjs";

const checkDnsSchema = z.object({
domain: z
.string()
.min(1, "Domain cannot be empty.")
.max(63, "Domain too long. Max 63 chars.")
.regex(
/^(?!-)[A-Za-z0-9-]+([-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,6}$/,
"Invalid domain format. Use format like sub.example.com",
)
.trim(),
});

const vercel = new Vercel({
bearerToken: env.VERCEL_ACCESS_TOKEN,
});

export const checkDnsRecordAction = authActionClient
.schema(checkDnsSchema)
.metadata({
name: "check-dns-record",
track: {
event: "add-custom-domain",
channel: "server",
},
})
.action(async ({ parsedInput, ctx }) => {
const { domain } = parsedInput;

if (!ctx.session.activeOrganizationId) {
throw new Error("No active organization");
}

const rootDomain = domain.split(".").slice(-2).join(".");
const activeOrgId = ctx.session.activeOrganizationId;

const response = await fetch(
`https://networkcalc.com/api/dns/lookup/${domain}`,
);
const txtResponse = await fetch(
`https://networkcalc.com/api/dns/lookup/${rootDomain}?type=TXT`,
);
const data = await response.json();
const txtData = await txtResponse.json();

if (
response.status !== 200 ||
data.status !== "OK" ||
txtResponse.status !== 200 ||
txtData.status !== "OK"
) {
console.error("DNS lookup failed:", data);
throw new Error(
data.message ||
"DNS record verification failed, check the records are valid or try again later.",
);
}

const cnameRecords = data.records?.CNAME;
const txtRecords = txtData.records?.TXT;

const expectedCnameValue = "cname.vercel-dns.com";
const expectedTxtValue = `compai-domain-verification=${activeOrgId}`;

let isCnameVerified = false;

if (cnameRecords) {
isCnameVerified = cnameRecords.some(
(record: { address: string }) =>
record.address.toLowerCase() === expectedCnameValue,
);
}

let isTxtVerified = false;

if (txtRecords) {
isTxtVerified = txtRecords.some((record: any) => {
if (typeof record === "string") {
return record === expectedTxtValue;
}
if (record && typeof record.value === "string") {
return record.value === expectedTxtValue;
}
if (
record &&
Array.isArray(record.txt) &&
record.txt.length > 0
) {
return record.txt.includes(expectedTxtValue);
}
return false;
});
}

const isVerified = isCnameVerified && isTxtVerified;

if (!isVerified) {
throw new Error(
"Error verifying DNS records. Please ensure both CNAME and TXT records are correctly configured, or wait a few minutes and try again.",
);
}

if (!env.VERCEL_PROJECT_ID) {
throw new Error("Vercel project ID is not set.");
}

const isExistingRecord = await vercel.projects.getProjectDomains({
idOrName: env.VERCEL_PROJECT_ID,
teamId: env.VERCEL_TEAM_ID,
});

if (isExistingRecord.domains.some((record) => record.name === domain)) {
await vercel.projects.removeProjectDomain({
idOrName: env.VERCEL_PROJECT_ID,
teamId: env.VERCEL_TEAM_ID,
domain,
});
}

const addDomainToProject = await vercel.projects
.addProjectDomain({
idOrName: env.VERCEL_PROJECT_ID,
teamId: env.VERCEL_TEAM_ID,
slug: env.VERCEL_PROJECT_ID,
requestBody: {
name: domain,
},
})
.then(async (res) => {
await db.trust.upsert({
where: {
organizationId: activeOrgId,
domain,
},
update: {
domainVerified: true,
status: "published",
},
create: {
organizationId: activeOrgId,
domain,
status: "published",
},
});
});

revalidatePath("/settings/trust-portal");
revalidateTag(`organization_${ctx.session.activeOrganizationId}`);

return {
success: true,
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// custom-domain-action.ts

"use server";

import { db } from "@comp/db";
import { authActionClient } from "@/actions/safe-action";
import { z } from "zod";
import { revalidatePath, revalidateTag } from "next/cache";

const customDomainSchema = z.object({
domain: z.string().min(1),
});

export const customDomainAction = authActionClient
.schema(customDomainSchema)
.metadata({
name: "custom-domain",
track: {
event: "add-custom-domain",
channel: "server",
},
})
.action(async ({ parsedInput, ctx }) => {
const { domain } = parsedInput;
const { activeOrganizationId } = ctx.session;

if (!activeOrganizationId) {
throw new Error("No active organization");
}

try {
await db.trust.update({
where: { organizationId: activeOrganizationId },
data: { domain, domainVerified: false },
});

revalidatePath("/settings/trust-portal");
revalidateTag(`organization_${activeOrganizationId}`);

return {
success: true,
};
} catch (error) {
console.error(error);
throw new Error("Failed to update custom domain");
}
});
Loading
Loading