mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-27 07:16:40 +00:00
add common domain validation func
This commit is contained in:
112
server/lib/domainUtils.ts
Normal file
112
server/lib/domainUtils.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { db } from "@server/db";
|
||||||
|
import { domains, orgDomains } from "@server/db";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { subdomainSchema } from "@server/lib/schemas";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
export type DomainValidationResult = {
|
||||||
|
success: true;
|
||||||
|
fullDomain: string;
|
||||||
|
subdomain: string | null;
|
||||||
|
} | {
|
||||||
|
success: false;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a domain and constructs the full domain based on domain type and subdomain.
|
||||||
|
*
|
||||||
|
* @param domainId - The ID of the domain to validate
|
||||||
|
* @param orgId - The organization ID to check domain access
|
||||||
|
* @param subdomain - Optional subdomain to append (for ns and wildcard domains)
|
||||||
|
* @returns DomainValidationResult with success status and either fullDomain/subdomain or error message
|
||||||
|
*/
|
||||||
|
export async function validateAndConstructDomain(
|
||||||
|
domainId: string,
|
||||||
|
orgId: string,
|
||||||
|
subdomain?: string | null
|
||||||
|
): Promise<DomainValidationResult> {
|
||||||
|
try {
|
||||||
|
// Query domain with organization access check
|
||||||
|
const [domainRes] = await db
|
||||||
|
.select()
|
||||||
|
.from(domains)
|
||||||
|
.where(eq(domains.domainId, domainId))
|
||||||
|
.leftJoin(
|
||||||
|
orgDomains,
|
||||||
|
and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if domain exists
|
||||||
|
if (!domainRes || !domainRes.domains) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Domain with ID ${domainId} not found`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if organization has access to domain
|
||||||
|
if (domainRes.orgDomains && domainRes.orgDomains.orgId !== orgId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Organization does not have access to domain with ID ${domainId}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if domain is verified
|
||||||
|
if (!domainRes.domains.verified) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Domain with ID ${domainId} is not verified`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct full domain based on domain type
|
||||||
|
let fullDomain = "";
|
||||||
|
let finalSubdomain = subdomain;
|
||||||
|
|
||||||
|
if (domainRes.domains.type === "ns") {
|
||||||
|
if (subdomain) {
|
||||||
|
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
|
||||||
|
} else {
|
||||||
|
fullDomain = domainRes.domains.baseDomain;
|
||||||
|
}
|
||||||
|
} else if (domainRes.domains.type === "cname") {
|
||||||
|
fullDomain = domainRes.domains.baseDomain;
|
||||||
|
finalSubdomain = null; // CNAME domains don't use subdomains
|
||||||
|
} else if (domainRes.domains.type === "wildcard") {
|
||||||
|
if (subdomain !== undefined && subdomain !== null) {
|
||||||
|
// Validate subdomain format for wildcard domains
|
||||||
|
const parsedSubdomain = subdomainSchema.safeParse(subdomain);
|
||||||
|
if (!parsedSubdomain.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: fromError(parsedSubdomain.error).toString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
|
||||||
|
} else {
|
||||||
|
fullDomain = domainRes.domains.baseDomain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the full domain equals the base domain, set subdomain to null
|
||||||
|
if (fullDomain === domainRes.domains.baseDomain) {
|
||||||
|
finalSubdomain = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to lowercase
|
||||||
|
fullDomain = fullDomain.toLowerCase();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
fullDomain,
|
||||||
|
subdomain: finalSubdomain ?? null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `An error occurred while validating domain: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { subdomainSchema } from "@server/lib/schemas";
|
|||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||||
|
|
||||||
const createResourceParamsSchema = z
|
const createResourceParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -193,76 +194,21 @@ async function createHttpResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { name, domainId } = parsedBody.data;
|
const { name, domainId } = parsedBody.data;
|
||||||
let subdomain = parsedBody.data.subdomain;
|
const subdomain = parsedBody.data.subdomain;
|
||||||
|
|
||||||
const [domainRes] = await db
|
// Validate domain and construct full domain
|
||||||
.select()
|
const domainResult = await validateAndConstructDomain(domainId, orgId, subdomain);
|
||||||
.from(domains)
|
|
||||||
.where(eq(domains.domainId, domainId))
|
if (!domainResult.success) {
|
||||||
.leftJoin(
|
|
||||||
orgDomains,
|
|
||||||
and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!domainRes || !domainRes.domains) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Domain with ID ${domainId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domainRes.orgDomains && domainRes.orgDomains.orgId !== orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
`Organization does not have access to domain with ID ${domainId}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!domainRes.domains.verified) {
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
`Domain with ID ${domainRes.domains.domainId} is not verified`
|
domainResult.error
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullDomain = "";
|
const { fullDomain, subdomain: finalSubdomain } = domainResult;
|
||||||
if (domainRes.domains.type == "ns") {
|
|
||||||
if (subdomain) {
|
|
||||||
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
|
|
||||||
} else {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
}
|
|
||||||
} else if (domainRes.domains.type == "cname") {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
} else if (domainRes.domains.type == "wildcard") {
|
|
||||||
if (subdomain) {
|
|
||||||
// the subdomain cant have a dot in it
|
|
||||||
const parsedSubdomain = subdomainSchema.safeParse(subdomain);
|
|
||||||
if (!parsedSubdomain.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedSubdomain.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
|
|
||||||
} else {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullDomain === domainRes.domains.baseDomain) {
|
|
||||||
subdomain = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fullDomain = fullDomain.toLowerCase();
|
|
||||||
|
|
||||||
logger.debug(`Full domain: ${fullDomain}`);
|
logger.debug(`Full domain: ${fullDomain}`);
|
||||||
|
|
||||||
@@ -291,7 +237,7 @@ async function createHttpResource(
|
|||||||
domainId,
|
domainId,
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
subdomain,
|
subdomain: finalSubdomain,
|
||||||
http: true,
|
http: true,
|
||||||
protocol: "tcp",
|
protocol: "tcp",
|
||||||
ssl: true
|
ssl: true
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { tlsNameSchema } from "@server/lib/schemas";
|
|||||||
import { subdomainSchema } from "@server/lib/schemas";
|
import { subdomainSchema } from "@server/lib/schemas";
|
||||||
import { registry } from "@server/openApi";
|
import { registry } from "@server/openApi";
|
||||||
import { OpenAPITags } from "@server/openApi";
|
import { OpenAPITags } from "@server/openApi";
|
||||||
|
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||||
|
|
||||||
const updateResourceParamsSchema = z
|
const updateResourceParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -230,78 +231,19 @@ async function updateHttpResource(
|
|||||||
if (updateData.domainId) {
|
if (updateData.domainId) {
|
||||||
const domainId = updateData.domainId;
|
const domainId = updateData.domainId;
|
||||||
|
|
||||||
const [domainRes] = await db
|
// Validate domain and construct full domain
|
||||||
.select()
|
const domainResult = await validateAndConstructDomain(domainId, resource.orgId, updateData.subdomain);
|
||||||
.from(domains)
|
|
||||||
.where(eq(domains.domainId, domainId))
|
if (!domainResult.success) {
|
||||||
.leftJoin(
|
|
||||||
orgDomains,
|
|
||||||
and(
|
|
||||||
eq(orgDomains.orgId, resource.orgId),
|
|
||||||
eq(orgDomains.domainId, domainId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!domainRes || !domainRes.domains) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Domain with ID ${updateData.domainId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
domainRes.orgDomains &&
|
|
||||||
domainRes.orgDomains.orgId !== resource.orgId
|
|
||||||
) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
`You do not have permission to use domain with ID ${updateData.domainId}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!domainRes.domains.verified) {
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
`Domain with ID ${updateData.domainId} is not verified`
|
domainResult.error
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullDomain = "";
|
const { fullDomain, subdomain: finalSubdomain } = domainResult;
|
||||||
if (domainRes.domains.type == "ns") {
|
|
||||||
if (updateData.subdomain) {
|
|
||||||
fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`;
|
|
||||||
} else {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
}
|
|
||||||
} else if (domainRes.domains.type == "cname") {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
} else if (domainRes.domains.type == "wildcard") {
|
|
||||||
if (updateData.subdomain !== undefined) {
|
|
||||||
// the subdomain cant have a dot in it
|
|
||||||
const parsedSubdomain = subdomainSchema.safeParse(
|
|
||||||
updateData.subdomain
|
|
||||||
);
|
|
||||||
if (!parsedSubdomain.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedSubdomain.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`;
|
|
||||||
} else {
|
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fullDomain = fullDomain.toLowerCase();
|
|
||||||
|
|
||||||
logger.debug(`Full domain: ${fullDomain}`);
|
logger.debug(`Full domain: ${fullDomain}`);
|
||||||
|
|
||||||
@@ -332,9 +274,8 @@ async function updateHttpResource(
|
|||||||
.where(eq(resources.resourceId, resource.resourceId));
|
.where(eq(resources.resourceId, resource.resourceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullDomain === domainRes.domains.baseDomain) {
|
// Update the subdomain in the update data
|
||||||
updateData.subdomain = null;
|
updateData.subdomain = finalSubdomain;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedResource = await db
|
const updatedResource = await db
|
||||||
|
|||||||
@@ -471,15 +471,11 @@ export default function GeneralForm() {
|
|||||||
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
|
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
|
||||||
: selectedDomain.baseDomain;
|
: selectedDomain.baseDomain;
|
||||||
|
|
||||||
setResourceFullDomain(sanitizedFullDomain);
|
setResourceFullDomain(`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`);
|
||||||
form.setValue("domainId", selectedDomain.domainId);
|
form.setValue("domainId", selectedDomain.domainId);
|
||||||
form.setValue("subdomain", sanitizedSubdomain);
|
form.setValue("subdomain", sanitizedSubdomain);
|
||||||
|
|
||||||
setEditDomainOpen(false);
|
setEditDomainOpen(false);
|
||||||
|
|
||||||
toast({
|
|
||||||
description: `Final domain: ${sanitizedFullDomain}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user