mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-27 14:15:50 +02:00
Create wildcard domains
This commit is contained in:
parent
b75800c583
commit
69d253fba3
6 changed files with 214 additions and 157 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -34,3 +34,4 @@ bin
|
||||||
test_event.json
|
test_event.json
|
||||||
.idea/
|
.idea/
|
||||||
server/db/index.ts
|
server/db/index.ts
|
||||||
|
build.ts
|
|
@ -1159,6 +1159,8 @@
|
||||||
"selectDomainTypeNsDescription": "This domain and all its subdomains. Use this when you want to control an entire domain zone.",
|
"selectDomainTypeNsDescription": "This domain and all its subdomains. Use this when you want to control an entire domain zone.",
|
||||||
"selectDomainTypeCnameName": "Single Domain (CNAME)",
|
"selectDomainTypeCnameName": "Single Domain (CNAME)",
|
||||||
"selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.",
|
"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",
|
"domainDelegation": "Single Domain",
|
||||||
"selectType": "Select a type",
|
"selectType": "Select a type",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { subdomainSchema } from "@server/lib/schemas";
|
||||||
import { generateId } from "@server/auth/sessions/app";
|
import { generateId } from "@server/auth/sessions/app";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { isValidDomain } from "@server/lib/validators";
|
import { isValidDomain } from "@server/lib/validators";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -19,7 +20,7 @@ const paramsSchema = z
|
||||||
|
|
||||||
const bodySchema = z
|
const bodySchema = z
|
||||||
.object({
|
.object({
|
||||||
type: z.enum(["ns", "cname"]),
|
type: z.enum(["ns", "cname", "wildcard"]),
|
||||||
baseDomain: subdomainSchema
|
baseDomain: subdomainSchema
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
@ -68,6 +69,26 @@ export async function createOrgDomain(
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
const { type, baseDomain } = parsedBody.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
|
// Validate organization exists
|
||||||
if (!isValidDomain(baseDomain)) {
|
if (!isValidDomain(baseDomain)) {
|
||||||
return next(
|
return next(
|
||||||
|
@ -132,7 +153,7 @@ export async function createOrgDomain(
|
||||||
.from(domains)
|
.from(domains)
|
||||||
.where(eq(domains.verified, true));
|
.where(eq(domains.verified, true));
|
||||||
|
|
||||||
if (type === "cname") {
|
if (type == "cname") {
|
||||||
// Block if a verified CNAME exists at the same name
|
// Block if a verified CNAME exists at the same name
|
||||||
const cnameExists = verifiedDomains.some(
|
const cnameExists = verifiedDomains.some(
|
||||||
(d) => d.type === "cname" && d.baseDomain === baseDomain
|
(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)
|
// Block if a verified NS exists at or below (same or subdomain)
|
||||||
const nsAtOrBelow = verifiedDomains.some(
|
const nsAtOrBelow = verifiedDomains.some(
|
||||||
(d) =>
|
(d) =>
|
||||||
|
@ -176,6 +197,8 @@ export async function createOrgDomain(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (type == "wildcard") {
|
||||||
|
// TODO: Figure out how to handle wildcards
|
||||||
}
|
}
|
||||||
|
|
||||||
const domainId = generateId(15);
|
const domainId = generateId(15);
|
||||||
|
@ -185,7 +208,8 @@ export async function createOrgDomain(
|
||||||
.values({
|
.values({
|
||||||
domainId,
|
domainId,
|
||||||
baseDomain,
|
baseDomain,
|
||||||
type
|
type,
|
||||||
|
verified: build == "oss" ? true : false
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
@ -214,6 +238,17 @@ export async function createOrgDomain(
|
||||||
baseDomain: `_acme-challenge.${baseDomain}`
|
baseDomain: `_acme-challenge.${baseDomain}`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
} else if (type === "wildcard") {
|
||||||
|
cnameRecords = [
|
||||||
|
{
|
||||||
|
value: `Server IP Address`,
|
||||||
|
baseDomain: `*.${baseDomain}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: `Server IP Address`,
|
||||||
|
baseDomain: `${baseDomain}`
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
numOrgDomains = await trx
|
numOrgDomains = await trx
|
||||||
|
|
|
@ -43,6 +43,7 @@ import { createNewt, getNewtToken } from "./newt";
|
||||||
import { getOlmToken } from "./olm";
|
import { getOlmToken } from "./olm";
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit from "express-rate-limit";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
// Root routes
|
// Root routes
|
||||||
export const unauthenticated = Router();
|
export const unauthenticated = Router();
|
||||||
|
@ -57,7 +58,9 @@ authenticated.use(verifySessionUserMiddleware);
|
||||||
|
|
||||||
authenticated.get("/pick-org-defaults", org.pickOrgDefaults);
|
authenticated.get("/pick-org-defaults", org.pickOrgDefaults);
|
||||||
authenticated.get("/org/checkId", org.checkId);
|
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("/orgs", verifyUserIsServerAdmin, org.listOrgs);
|
||||||
authenticated.get("/user/:userId/orgs", verifyIsLoggedInUser, org.listUserOrgs);
|
authenticated.get("/user/:userId/orgs", verifyIsLoggedInUser, org.listUserOrgs);
|
||||||
|
|
|
@ -42,10 +42,11 @@ import {
|
||||||
InfoSectionTitle
|
InfoSectionTitle
|
||||||
} from "@app/components/InfoSection";
|
} from "@app/components/InfoSection";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
baseDomain: z.string().min(1, "Domain is required"),
|
baseDomain: z.string().min(1, "Domain is required"),
|
||||||
type: z.enum(["ns", "cname"])
|
type: z.enum(["ns", "cname", "wildcard"])
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>;
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
@ -73,7 +74,7 @@ export default function CreateDomainForm({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
baseDomain: "",
|
baseDomain: "",
|
||||||
type: "ns"
|
type: build == "oss" ? "wildcard" : "ns"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -111,6 +112,30 @@ export default function CreateDomainForm({
|
||||||
const domainType = form.watch("type");
|
const domainType = form.watch("type");
|
||||||
const baseDomain = form.watch("baseDomain");
|
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 (
|
return (
|
||||||
<Credenza
|
<Credenza
|
||||||
open={open}
|
open={open}
|
||||||
|
@ -140,26 +165,7 @@ export default function CreateDomainForm({
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<StrategySelect
|
<StrategySelect
|
||||||
options={[
|
options={domainOptions}
|
||||||
{
|
|
||||||
id: "ns",
|
|
||||||
title: t(
|
|
||||||
"selectDomainTypeNsName"
|
|
||||||
),
|
|
||||||
description: t(
|
|
||||||
"selectDomainTypeNsDescription"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cname",
|
|
||||||
title: t(
|
|
||||||
"selectDomainTypeCnameName"
|
|
||||||
),
|
|
||||||
description: t(
|
|
||||||
"selectDomainTypeCnameDescription"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
cols={1}
|
cols={1}
|
||||||
|
@ -258,141 +264,149 @@ export default function CreateDomainForm({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{domainType === "cname" && (
|
{domainType === "cname" ||
|
||||||
<>
|
(domainType == "wildcard" && (
|
||||||
{createdDomain.cnameRecords &&
|
<>
|
||||||
createdDomain.cnameRecords.length >
|
{createdDomain.cnameRecords &&
|
||||||
0 && (
|
createdDomain.cnameRecords
|
||||||
<div>
|
.length > 0 && (
|
||||||
<h3 className="font-medium mb-3">
|
<div>
|
||||||
CNAME Records
|
<h3 className="font-medium mb-3">
|
||||||
</h3>
|
CNAME Records
|
||||||
<InfoSections cols={1}>
|
</h3>
|
||||||
{createdDomain.cnameRecords.map(
|
<InfoSections cols={1}>
|
||||||
(
|
{createdDomain.cnameRecords.map(
|
||||||
cnameRecord,
|
(
|
||||||
index
|
cnameRecord,
|
||||||
) => (
|
index
|
||||||
<InfoSection
|
) => (
|
||||||
key={index}
|
<InfoSection
|
||||||
>
|
key={
|
||||||
<InfoSectionTitle>
|
index
|
||||||
Record{" "}
|
}
|
||||||
{index +
|
>
|
||||||
1}
|
<InfoSectionTitle>
|
||||||
</InfoSectionTitle>
|
Record{" "}
|
||||||
<InfoSectionContent>
|
{index +
|
||||||
<div className="space-y-2">
|
1}
|
||||||
<div className="flex justify-between items-center">
|
</InfoSectionTitle>
|
||||||
<span className="text-sm font-medium">
|
<InfoSectionContent>
|
||||||
Type:
|
<div className="space-y-2">
|
||||||
</span>
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm font-mono">
|
<span className="text-sm font-medium">
|
||||||
CNAME
|
Type:
|
||||||
</span>
|
</span>
|
||||||
|
<span className="text-sm font-mono">
|
||||||
|
CNAME
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
Name:
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-mono">
|
||||||
|
{
|
||||||
|
cnameRecord.baseDomain
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
Value:
|
||||||
|
</span>
|
||||||
|
<CopyToClipboard
|
||||||
|
text={
|
||||||
|
cnameRecord.value
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
</InfoSectionContent>
|
||||||
<span className="text-sm font-medium">
|
</InfoSection>
|
||||||
Name:
|
)
|
||||||
</span>
|
)}
|
||||||
<span className="text-sm font-mono">
|
</InfoSections>
|
||||||
{
|
</div>
|
||||||
cnameRecord.baseDomain
|
)}
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
Value:
|
|
||||||
</span>
|
|
||||||
<CopyToClipboard
|
|
||||||
text={
|
|
||||||
cnameRecord.value
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InfoSectionContent>
|
|
||||||
</InfoSection>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</InfoSections>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{createdDomain.txtRecords &&
|
{createdDomain.txtRecords &&
|
||||||
createdDomain.txtRecords.length >
|
createdDomain.txtRecords
|
||||||
0 && (
|
.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium mb-3">
|
<h3 className="font-medium mb-3">
|
||||||
TXT Records
|
TXT Records
|
||||||
</h3>
|
</h3>
|
||||||
<InfoSections cols={1}>
|
<InfoSections cols={1}>
|
||||||
{createdDomain.txtRecords.map(
|
{createdDomain.txtRecords.map(
|
||||||
(
|
(
|
||||||
txtRecord,
|
txtRecord,
|
||||||
index
|
index
|
||||||
) => (
|
) => (
|
||||||
<InfoSection
|
<InfoSection
|
||||||
key={index}
|
key={
|
||||||
>
|
index
|
||||||
<InfoSectionTitle>
|
}
|
||||||
Record{" "}
|
>
|
||||||
{index +
|
<InfoSectionTitle>
|
||||||
1}
|
Record{" "}
|
||||||
</InfoSectionTitle>
|
{index +
|
||||||
<InfoSectionContent>
|
1}
|
||||||
<div className="space-y-2">
|
</InfoSectionTitle>
|
||||||
<div className="flex justify-between items-center">
|
<InfoSectionContent>
|
||||||
<span className="text-sm font-medium">
|
<div className="space-y-2">
|
||||||
Type:
|
<div className="flex justify-between items-center">
|
||||||
</span>
|
<span className="text-sm font-medium">
|
||||||
<span className="text-sm font-mono">
|
Type:
|
||||||
TXT
|
</span>
|
||||||
</span>
|
<span className="text-sm font-mono">
|
||||||
|
TXT
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
Name:
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-mono">
|
||||||
|
{
|
||||||
|
txtRecord.baseDomain
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
Value:
|
||||||
|
</span>
|
||||||
|
<CopyToClipboard
|
||||||
|
text={
|
||||||
|
txtRecord.value
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
</InfoSectionContent>
|
||||||
<span className="text-sm font-medium">
|
</InfoSection>
|
||||||
Name:
|
)
|
||||||
</span>
|
)}
|
||||||
<span className="text-sm font-mono">
|
</InfoSections>
|
||||||
{
|
</div>
|
||||||
txtRecord.baseDomain
|
)}
|
||||||
}
|
</>
|
||||||
</span>
|
))}
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
Value:
|
|
||||||
</span>
|
|
||||||
<CopyToClipboard
|
|
||||||
text={
|
|
||||||
txtRecord.value
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InfoSectionContent>
|
|
||||||
</InfoSection>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</InfoSections>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Alert variant="destructive">
|
{build == "saas" ||
|
||||||
<AlertTriangle className="h-4 w-4" />
|
(build == "enterprise" && (
|
||||||
<AlertTitle className="font-semibold">
|
<Alert variant="destructive">
|
||||||
Save These Records
|
<AlertTriangle className="h-4 w-4" />
|
||||||
</AlertTitle>
|
<AlertTitle className="font-semibold">
|
||||||
<AlertDescription>
|
Save These Records
|
||||||
Make sure to save these DNS records as you
|
</AlertTitle>
|
||||||
will not see them again.
|
<AlertDescription>
|
||||||
</AlertDescription>
|
Make sure to save these DNS records
|
||||||
</Alert>
|
as you will not see them again.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
|
|
||||||
<Alert variant="info">
|
<Alert variant="info">
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
|
|
@ -106,6 +106,8 @@ export default function DomainsTable({ domains }: Props) {
|
||||||
return t("selectDomainTypeNsName");
|
return t("selectDomainTypeNsName");
|
||||||
case "cname":
|
case "cname":
|
||||||
return t("selectDomainTypeCnameName");
|
return t("selectDomainTypeCnameName");
|
||||||
|
case "wildcard":
|
||||||
|
return t("selectDomainTypeWildcardName");
|
||||||
default:
|
default:
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue