From 69d253fba3ad2933c64a1d7124813de8f04d56a6 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 14 Jul 2025 12:18:12 -0700 Subject: [PATCH] Create wildcard domains --- .gitignore | 1 + messages/en-US.json | 2 + server/routers/domain/createOrgDomain.ts | 43 ++- server/routers/external.ts | 5 +- .../settings/domains/CreateDomainForm.tsx | 318 +++++++++--------- .../[orgId]/settings/domains/DomainsTable.tsx | 2 + 6 files changed, 214 insertions(+), 157 deletions(-) diff --git a/.gitignore b/.gitignore index 04c4b7ef..c700a478 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ bin test_event.json .idea/ server/db/index.ts +build.ts \ No newline at end of file diff --git a/messages/en-US.json b/messages/en-US.json index acbcdbe0..4d502221 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1159,6 +1159,8 @@ "selectDomainTypeNsDescription": "This domain and all its subdomains. Use this when you want to control an entire domain zone.", "selectDomainTypeCnameName": "Single Domain (CNAME)", "selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.", + "selectDomainTypeWildcardName": "Wildcard Domain (CNAME)", + "selectDomainTypeWildcardDescription": "This domain and its first level of subdomains.", "domainDelegation": "Single Domain", "selectType": "Select a type", "actions": "Actions", diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index 1b8720a1..a1d9bf2a 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -10,6 +10,7 @@ import { subdomainSchema } from "@server/lib/schemas"; import { generateId } from "@server/auth/sessions/app"; import { eq, and } from "drizzle-orm"; import { isValidDomain } from "@server/lib/validators"; +import { build } from "@server/build"; const paramsSchema = z .object({ @@ -19,7 +20,7 @@ const paramsSchema = z const bodySchema = z .object({ - type: z.enum(["ns", "cname"]), + type: z.enum(["ns", "cname", "wildcard"]), baseDomain: subdomainSchema }) .strict(); @@ -68,6 +69,26 @@ export async function createOrgDomain( const { orgId } = parsedParams.data; const { type, baseDomain } = parsedBody.data; + if (build == "oss") { + if (type !== "wildcard") { + return next( + createHttpError( + HttpCode.NOT_IMPLEMENTED, + "Creating NS or CNAME records is not supported" + ) + ); + } + } else if (build == "enterprise" || build == "saas") { + if (type !== "ns" && type !== "cname") { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid domain type. Only NS, CNAME are allowed." + ) + ); + } + } + // Validate organization exists if (!isValidDomain(baseDomain)) { return next( @@ -132,7 +153,7 @@ export async function createOrgDomain( .from(domains) .where(eq(domains.verified, true)); - if (type === "cname") { + if (type == "cname") { // Block if a verified CNAME exists at the same name const cnameExists = verifiedDomains.some( (d) => d.type === "cname" && d.baseDomain === baseDomain @@ -160,7 +181,7 @@ export async function createOrgDomain( ) ); } - } else if (type === "ns") { + } else if (type == "ns") { // Block if a verified NS exists at or below (same or subdomain) const nsAtOrBelow = verifiedDomains.some( (d) => @@ -176,6 +197,8 @@ export async function createOrgDomain( ) ); } + } else if (type == "wildcard") { + // TODO: Figure out how to handle wildcards } const domainId = generateId(15); @@ -185,7 +208,8 @@ export async function createOrgDomain( .values({ domainId, baseDomain, - type + type, + verified: build == "oss" ? true : false }) .returning(); @@ -214,6 +238,17 @@ export async function createOrgDomain( baseDomain: `_acme-challenge.${baseDomain}` } ]; + } else if (type === "wildcard") { + cnameRecords = [ + { + value: `Server IP Address`, + baseDomain: `*.${baseDomain}` + }, + { + value: `Server IP Address`, + baseDomain: `${baseDomain}` + } + ]; } numOrgDomains = await trx diff --git a/server/routers/external.ts b/server/routers/external.ts index 69921804..0f51dc50 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -43,6 +43,7 @@ import { createNewt, getNewtToken } from "./newt"; import { getOlmToken } from "./olm"; import rateLimit from "express-rate-limit"; import createHttpError from "http-errors"; +import { build } from "@server/build"; // Root routes export const unauthenticated = Router(); @@ -57,7 +58,9 @@ authenticated.use(verifySessionUserMiddleware); authenticated.get("/pick-org-defaults", org.pickOrgDefaults); authenticated.get("/org/checkId", org.checkId); -authenticated.put("/org", getUserOrgs, org.createOrg); +if (build === "oss" || build === "enterprise") { + authenticated.put("/org", getUserOrgs, org.createOrg); +} authenticated.get("/orgs", verifyUserIsServerAdmin, org.listOrgs); authenticated.get("/user/:userId/orgs", verifyIsLoggedInUser, org.listUserOrgs); diff --git a/src/app/[orgId]/settings/domains/CreateDomainForm.tsx b/src/app/[orgId]/settings/domains/CreateDomainForm.tsx index 41f585d0..aeca2963 100644 --- a/src/app/[orgId]/settings/domains/CreateDomainForm.tsx +++ b/src/app/[orgId]/settings/domains/CreateDomainForm.tsx @@ -42,10 +42,11 @@ import { InfoSectionTitle } from "@app/components/InfoSection"; import { useOrgContext } from "@app/hooks/useOrgContext"; +import { build } from "@server/build"; const formSchema = z.object({ baseDomain: z.string().min(1, "Domain is required"), - type: z.enum(["ns", "cname"]) + type: z.enum(["ns", "cname", "wildcard"]) }); type FormValues = z.infer; @@ -73,7 +74,7 @@ export default function CreateDomainForm({ resolver: zodResolver(formSchema), defaultValues: { baseDomain: "", - type: "ns" + type: build == "oss" ? "wildcard" : "ns" } }); @@ -111,6 +112,30 @@ export default function CreateDomainForm({ const domainType = form.watch("type"); const baseDomain = form.watch("baseDomain"); + let domainOptions: any = []; + if (build == "enterprise" || build == "saas") { + domainOptions = [ + { + id: "ns", + title: t("selectDomainTypeNsName"), + description: t("selectDomainTypeNsDescription") + }, + { + id: "cname", + title: t("selectDomainTypeCnameName"), + description: t("selectDomainTypeCnameDescription") + } + ]; + } else if (build == "oss") { + domainOptions = [ + { + id: "wildcard", + title: t("selectDomainTypeWildcardName"), + description: t("selectDomainTypeWildcardDescription") + } + ]; + } + return ( ( )} - {domainType === "cname" && ( - <> - {createdDomain.cnameRecords && - createdDomain.cnameRecords.length > - 0 && ( -
-

- CNAME Records -

- - {createdDomain.cnameRecords.map( - ( - cnameRecord, - index - ) => ( - - - Record{" "} - {index + - 1} - - -
-
- - Type: - - - CNAME - + {domainType === "cname" || + (domainType == "wildcard" && ( + <> + {createdDomain.cnameRecords && + createdDomain.cnameRecords + .length > 0 && ( +
+

+ CNAME Records +

+ + {createdDomain.cnameRecords.map( + ( + cnameRecord, + index + ) => ( + + + Record{" "} + {index + + 1} + + +
+
+ + Type: + + + CNAME + +
+
+ + Name: + + + { + cnameRecord.baseDomain + } + +
+
+ + Value: + + +
-
- - Name: - - - { - cnameRecord.baseDomain - } - -
-
- - Value: - - -
-
- - - ) - )} - -
- )} + + + ) + )} + +
+ )} - {createdDomain.txtRecords && - createdDomain.txtRecords.length > - 0 && ( -
-

- TXT Records -

- - {createdDomain.txtRecords.map( - ( - txtRecord, - index - ) => ( - - - Record{" "} - {index + - 1} - - -
-
- - Type: - - - TXT - + {createdDomain.txtRecords && + createdDomain.txtRecords + .length > 0 && ( +
+

+ TXT Records +

+ + {createdDomain.txtRecords.map( + ( + txtRecord, + index + ) => ( + + + Record{" "} + {index + + 1} + + +
+
+ + Type: + + + TXT + +
+
+ + Name: + + + { + txtRecord.baseDomain + } + +
+
+ + Value: + + +
-
- - Name: - - - { - txtRecord.baseDomain - } - -
-
- - Value: - - -
-
- - - ) - )} - -
- )} - - )} + + + ) + )} + +
+ )} + + ))}
- - - - Save These Records - - - Make sure to save these DNS records as you - will not see them again. - - + {build == "saas" || + (build == "enterprise" && ( + + + + Save These Records + + + Make sure to save these DNS records + as you will not see them again. + + + ))} diff --git a/src/app/[orgId]/settings/domains/DomainsTable.tsx b/src/app/[orgId]/settings/domains/DomainsTable.tsx index 02cec6f2..1f73d5ae 100644 --- a/src/app/[orgId]/settings/domains/DomainsTable.tsx +++ b/src/app/[orgId]/settings/domains/DomainsTable.tsx @@ -106,6 +106,8 @@ export default function DomainsTable({ domains }: Props) { return t("selectDomainTypeNsName"); case "cname": return t("selectDomainTypeCnameName"); + case "wildcard": + return t("selectDomainTypeWildcardName"); default: return type; }