Resources working with new picker

This commit is contained in:
Owen 2025-07-14 15:36:15 -07:00
parent ec57996b01
commit 78661799f2
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
6 changed files with 218 additions and 160 deletions

View file

@ -1213,5 +1213,6 @@
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.", "domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
"failed": "Failed", "failed": "Failed",
"createNewOrgDescription": "Create a new organization", "createNewOrgDescription": "Create a new organization",
"organization": "Organization" "organization": "Organization",
"port": "Port"
} }

View file

@ -21,6 +21,7 @@ import logger from "@server/logger";
import { subdomainSchema } from "@server/lib/schemas"; 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";
const createResourceParamsSchema = z const createResourceParamsSchema = z
.object({ .object({
@ -36,7 +37,6 @@ const createHttpResourceSchema = z
.string() .string()
.optional() .optional()
.transform((val) => val?.toLowerCase()), .transform((val) => val?.toLowerCase()),
isBaseDomain: z.boolean().optional(),
siteId: z.number(), siteId: z.number(),
http: z.boolean(), http: z.boolean(),
protocol: z.enum(["tcp", "udp"]), protocol: z.enum(["tcp", "udp"]),
@ -52,19 +52,6 @@ const createHttpResourceSchema = z
}, },
{ message: "Invalid subdomain" } { message: "Invalid subdomain" }
) )
.refine(
(data) => {
if (!config.getRawConfig().flags?.allow_base_domain_resources) {
if (data.isBaseDomain) {
return false;
}
}
return true;
},
{
message: "Base domain resources are not allowed"
}
);
const createRawResourceSchema = z const createRawResourceSchema = z
.object({ .object({
@ -101,9 +88,12 @@ registry.registerPath({
body: { body: {
content: { content: {
"application/json": { "application/json": {
schema: createHttpResourceSchema.or( schema:
createRawResourceSchema build == "oss"
) ? createHttpResourceSchema.or(
createRawResourceSchema
)
: createHttpResourceSchema
} }
} }
} }
@ -166,7 +156,7 @@ export async function createResource(
{ siteId, orgId } { siteId, orgId }
); );
} else { } else {
if (!config.getRawConfig().flags?.allow_raw_resources) { if (!config.getRawConfig().flags?.allow_raw_resources && build == "oss") {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
@ -211,35 +201,81 @@ async function createHttpResource(
); );
} }
const { name, subdomain, isBaseDomain, http, protocol, domainId } = const { name, subdomain, domainId } = parsedBody.data;
parsedBody.data;
const [orgDomain] = await db const [domainRes] = await db
.select() .select()
.from(orgDomains) .from(domains)
.where( .where(eq(domains.domainId, domainId))
.leftJoin(
orgDomains,
and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId)) and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId))
) );
.leftJoin(domains, eq(orgDomains.domainId, domains.domainId));
if (!orgDomain || !orgDomain.domains) { if (!domainRes || !domainRes.domains) {
return next( return next(
createHttpError( createHttpError(
HttpCode.NOT_FOUND, HttpCode.NOT_FOUND,
`Domain with ID ${parsedBody.data.domainId} not found` `Domain with ID ${domainId} not found`
) )
); );
} }
const domain = orgDomain.domains; 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(
createHttpError(
HttpCode.BAD_REQUEST,
`Domain with ID ${domainRes.domains.domainId} is not verified`
)
);
}
let fullDomain = ""; let fullDomain = "";
if (isBaseDomain) { if (domainRes.domains.type == "ns") {
fullDomain = domain.baseDomain; if (subdomain) {
} else { fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
fullDomain = `${subdomain}.${domain.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()
)
);
}
if (parsedSubdomain.data.includes(".")) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Subdomain cannot contain a dot when using wildcard domains"
)
);
}
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
} else {
fullDomain = domainRes.domains.baseDomain;
}
} }
fullDomain = fullDomain.toLowerCase();
logger.debug(`Full domain: ${fullDomain}`); logger.debug(`Full domain: ${fullDomain}`);
// make sure the full domain is unique // make sure the full domain is unique
@ -269,10 +305,10 @@ async function createHttpResource(
orgId, orgId,
name, name,
subdomain, subdomain,
http, http: true,
protocol, protocol: "tcp",
ssl: true, ssl: true,
isBaseDomain isBaseDomain: false
}) })
.returning(); .returning();

View file

@ -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 { build } from "@server/build";
const updateResourceParamsSchema = z const updateResourceParamsSchema = z
.object({ .object({
@ -40,7 +41,6 @@ const updateHttpResourceBodySchema = z
sso: z.boolean().optional(), sso: z.boolean().optional(),
blockAccess: z.boolean().optional(), blockAccess: z.boolean().optional(),
emailWhitelistEnabled: z.boolean().optional(), emailWhitelistEnabled: z.boolean().optional(),
isBaseDomain: z.boolean().optional(),
applyRules: z.boolean().optional(), applyRules: z.boolean().optional(),
domainId: z.string().optional(), domainId: z.string().optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
@ -61,19 +61,6 @@ const updateHttpResourceBodySchema = z
}, },
{ message: "Invalid subdomain" } { message: "Invalid subdomain" }
) )
.refine(
(data) => {
if (!config.getRawConfig().flags?.allow_base_domain_resources) {
if (data.isBaseDomain) {
return false;
}
}
return true;
},
{
message: "Base domain resources are not allowed"
}
)
.refine( .refine(
(data) => { (data) => {
if (data.tlsServerName) { if (data.tlsServerName) {
@ -134,9 +121,12 @@ registry.registerPath({
body: { body: {
content: { content: {
"application/json": { "application/json": {
schema: updateHttpResourceBodySchema.and( schema:
updateRawResourceBodySchema build == "oss"
) ? updateHttpResourceBodySchema.and(
updateRawResourceBodySchema
)
: updateHttpResourceBodySchema
} }
} }
} }
@ -242,86 +232,120 @@ async function updateHttpResource(
const updateData = parsedBody.data; const updateData = parsedBody.data;
if (updateData.domainId) { if (updateData.domainId) {
const [existingDomain] = await db const domainId = updateData.domainId;
.select()
.from(orgDomains)
.where(
and(
eq(orgDomains.orgId, org.orgId),
eq(orgDomains.domainId, updateData.domainId)
)
)
.leftJoin(domains, eq(orgDomains.domainId, domains.domainId));
if (!existingDomain) { const [domainRes] = await db
.select()
.from(domains)
.where(eq(domains.domainId, domainId))
.leftJoin(
orgDomains,
and(
eq(orgDomains.orgId, resource.orgId),
eq(orgDomains.domainId, domainId)
)
);
if (!domainRes || !domainRes.domains) {
return next( return next(
createHttpError(HttpCode.NOT_FOUND, `Domain not found`) createHttpError(
HttpCode.NOT_FOUND,
`Domain with ID ${updateData.domainId} not found`
)
); );
} }
}
const domainId = updateData.domainId || resource.domainId!;
const subdomain = updateData.subdomain || resource.subdomain;
const [domain] = await db
.select()
.from(domains)
.where(eq(domains.domainId, domainId));
const isBaseDomain =
updateData.isBaseDomain !== undefined
? updateData.isBaseDomain
: resource.isBaseDomain;
let fullDomain: string | null = null;
if (isBaseDomain) {
fullDomain = domain.baseDomain;
} else if (subdomain && domain) {
fullDomain = `${subdomain}.${domain.baseDomain}`;
}
if (fullDomain) {
const [existingDomain] = await db
.select()
.from(resources)
.where(eq(resources.fullDomain, fullDomain));
if ( if (
existingDomain && domainRes.orgDomains &&
existingDomain.resourceId !== resource.resourceId domainRes.orgDomains.orgId !== resource.orgId
) { ) {
return next( return next(
createHttpError( createHttpError(
HttpCode.CONFLICT, HttpCode.FORBIDDEN,
"Resource with that domain already exists" `You do not have permission to use domain with ID ${updateData.domainId}`
) )
); );
} }
}
const updatePayload = { if (!domainRes.domains.verified) {
...updateData, return next(
fullDomain createHttpError(
}; HttpCode.BAD_REQUEST,
`Domain with ID ${updateData.domainId} is not verified`
)
);
}
let fullDomain = "";
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) {
// 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()
)
);
}
if (parsedSubdomain.data.includes(".")) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Subdomain cannot contain a dot when using wildcard domains"
)
);
}
fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`;
} else {
fullDomain = domainRes.domains.baseDomain;
}
}
fullDomain = fullDomain.toLowerCase();
logger.debug(`Full domain: ${fullDomain}`);
if (fullDomain) {
const [existingDomain] = await db
.select()
.from(resources)
.where(eq(resources.fullDomain, fullDomain));
if (
existingDomain &&
existingDomain.resourceId !== resource.resourceId
) {
return next(
createHttpError(
HttpCode.CONFLICT,
"Resource with that domain already exists"
)
);
}
}
// update the full domain if it has changed
if (fullDomain && fullDomain !== resource.fullDomain) {
await db
.update(resources)
.set({ fullDomain })
.where(eq(resources.resourceId, resource.resourceId));
}
}
const updatedResource = await db const updatedResource = await db
.update(resources) .update(resources)
.set({ .set(updateData)
name: updatePayload.name,
subdomain: updatePayload.subdomain,
ssl: updatePayload.ssl,
sso: updatePayload.sso,
blockAccess: updatePayload.blockAccess,
emailWhitelistEnabled: updatePayload.emailWhitelistEnabled,
isBaseDomain: updatePayload.isBaseDomain,
applyRules: updatePayload.applyRules,
domainId: updatePayload.domainId,
enabled: updatePayload.enabled,
stickySession: updatePayload.stickySession,
tlsServerName: updatePayload.tlsServerName,
setHostHeader: updatePayload.setHostHeader,
fullDomain: updatePayload.fullDomain
})
.where(eq(resources.resourceId, resource.resourceId)) .where(eq(resources.resourceId, resource.resourceId))
.returning(); .returning();

View file

@ -162,25 +162,21 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
const resourceRow = row.original; const resourceRow = row.original;
return ( return (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{!resourceRow.domainId ? ( {!resourceRow.http ? (
<CopyToClipboard
text={resourceRow.proxyPort!.toString()}
isLink={false}
/>
) : !resourceRow.domainId ? (
<InfoPopup <InfoPopup
info={t("domainNotFoundDescription")} info={t("domainNotFoundDescription")}
text={t("domainNotFound")} text={t("domainNotFound")}
/> />
) : ( ) : (
<div> <CopyToClipboard
{!resourceRow.http ? ( text={resourceRow.domain}
<CopyToClipboard isLink={true}
text={resourceRow.proxyPort!.toString()} />
isLink={false}
/>
) : (
<CopyToClipboard
text={resourceRow.domain}
isLink={true}
/>
)}
</div>
)} )}
</div> </div>
); );
@ -228,9 +224,11 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
cell: ({ row }) => ( cell: ({ row }) => (
<Switch <Switch
defaultChecked={ defaultChecked={
!row.original.domainId ? false : row.original.enabled row.original.http
? (!!row.original.domainId && row.original.enabled)
: row.original.enabled
} }
disabled={!row.original.domainId} disabled={row.original.http ? !row.original.domainId : false}
onCheckedChange={(val) => onCheckedChange={(val) =>
toggleResourceEnabled(val, row.original.id) toggleResourceEnabled(val, row.original.id)
} }

View file

@ -339,34 +339,32 @@ export default function ReverseProxyTargets(props: {
await api.delete(`/target/${targetId}`); await api.delete(`/target/${targetId}`);
} }
// Save sticky session setting if (resource.http) {
const stickySessionData = targetsSettingsForm.getValues(); // Gather all settings
await api.post(`/resource/${params.resourceId}`, { const stickySessionData = targetsSettingsForm.getValues();
stickySession: stickySessionData.stickySession const tlsData = tlsSettingsForm.getValues();
}); const proxyData = proxySettingsForm.getValues();
updateResource({ stickySession: stickySessionData.stickySession });
// Save TLS settings // Combine into one payload
const tlsData = tlsSettingsForm.getValues(); const payload = {
await api.post(`/resource/${params.resourceId}`, { stickySession: stickySessionData.stickySession,
ssl: tlsData.ssl, ssl: tlsData.ssl,
tlsServerName: tlsData.tlsServerName || null tlsServerName: tlsData.tlsServerName || null,
}); setHostHeader: proxyData.setHostHeader || null
updateResource({ };
...resource,
ssl: tlsData.ssl,
tlsServerName: tlsData.tlsServerName || null
});
// Save proxy settings // Single API call to update all settings
const proxyData = proxySettingsForm.getValues(); await api.post(`/resource/${params.resourceId}`, payload);
await api.post(`/resource/${params.resourceId}`, {
setHostHeader: proxyData.setHostHeader || null // Update local resource context
}); updateResource({
updateResource({ ...resource,
...resource, stickySession: stickySessionData.stickySession,
setHostHeader: proxyData.setHostHeader || null ssl: tlsData.ssl,
}); tlsServerName: tlsData.tlsServerName || null,
setHostHeader: proxyData.setHostHeader || null
});
}
toast({ toast({
title: t("settingsUpdated"), title: t("settingsUpdated"),

View file

@ -165,7 +165,8 @@ export default function Page() {
const httpData = httpForm.getValues(); const httpData = httpForm.getValues();
Object.assign(payload, { Object.assign(payload, {
subdomain: httpData.subdomain, subdomain: httpData.subdomain,
domainId: httpData.domainId domainId: httpData.domainId,
protocol: "tcp",
}); });
} else { } else {
const tcpUdpData = tcpUdpForm.getValues(); const tcpUdpData = tcpUdpForm.getValues();