Site resources for the blueprint

This commit is contained in:
Owen
2025-09-14 15:57:41 -07:00
parent 8929f389f4
commit 58c04fd196
13 changed files with 520 additions and 71 deletions

View File

@@ -1400,8 +1400,6 @@
"editInternalResourceDialogProtocol": "Protocol",
"editInternalResourceDialogSitePort": "Site Port",
"editInternalResourceDialogTargetConfiguration": "Target Configuration",
"editInternalResourceDialogDestinationIP": "Destination IP",
"editInternalResourceDialogDestinationPort": "Destination Port",
"editInternalResourceDialogCancel": "Cancel",
"editInternalResourceDialogSaveResource": "Save Resource",
"editInternalResourceDialogSuccess": "Success",
@@ -1432,9 +1430,7 @@
"createInternalResourceDialogSitePort": "Site Port",
"createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.",
"createInternalResourceDialogTargetConfiguration": "Target Configuration",
"createInternalResourceDialogDestinationIP": "Destination IP",
"createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.",
"createInternalResourceDialogDestinationPort": "Destination Port",
"createInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
"createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.",
"createInternalResourceDialogCancel": "Cancel",
"createInternalResourceDialogCreateResource": "Create Resource",

View File

@@ -1,6 +1,6 @@
import { join } from "path";
import { readFileSync } from "fs";
import { db, resources } from "@server/db";
import { db, resources, siteResources } from "@server/db";
import { exitNodes, sites } from "@server/db";
import { eq, and } from "drizzle-orm";
import { __DIRNAME } from "@server/lib/consts";
@@ -53,6 +53,25 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
}
}
export async function getUniqueSiteResourceName(orgId: string): Promise<string> {
let loops = 0;
while (true) {
if (loops > 100) {
throw new Error("Could not generate a unique name");
}
const name = generateName();
const count = await db
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
.from(siteResources)
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)));
if (count.length === 0) {
return name;
}
loops++;
}
}
export async function getUniqueExitNodeEndpointName(): Promise<string> {
let loops = 0;
const count = await db

View File

@@ -143,6 +143,7 @@ export const siteResources = pgTable("siteResources", { // this is for the clien
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
niceId: varchar("niceId").notNull(),
name: varchar("name").notNull(),
protocol: varchar("protocol").notNull(),
proxyPort: integer("proxyPort").notNull(),

View File

@@ -158,6 +158,7 @@ export const siteResources = sqliteTable("siteResources", {
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
niceId: text("niceId").notNull(),
name: text("name").notNull(),
protocol: text("protocol").notNull(),
proxyPort: integer("proxyPort").notNull(),

View File

@@ -1,11 +1,16 @@
import { db, newts, Target } from "@server/db";
import { Config, ConfigSchema } from "./types";
import { ResourcesResults, updateResources } from "./resources";
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { resources, targets, sites } from "@server/db";
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
import { addTargets } from "@server/routers/newt/targets";
import { addTargets as addProxyTargets } from "@server/routers/newt/targets";
import { addTargets as addClientTargets } from "@server/routers/client/targets";
import {
ClientResourcesResults,
updateClientResources
} from "./clientResources";
export async function applyBlueprint(
orgId: string,
@@ -21,17 +26,29 @@ export async function applyBlueprint(
const config: Config = validationResult.data;
try {
let resourcesResults: ResourcesResults = [];
let proxyResourcesResults: ProxyResourcesResults = [];
let clientResourcesResults: ClientResourcesResults = [];
await db.transaction(async (trx) => {
resourcesResults = await updateResources(orgId, config, trx, siteId);
proxyResourcesResults = await updateProxyResources(
orgId,
config,
trx,
siteId
);
clientResourcesResults = await updateClientResources(
orgId,
config,
trx,
siteId
);
});
logger.debug(
`Successfully updated resources for org ${orgId}: ${JSON.stringify(resourcesResults)}`
`Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}`
);
// We need to update the targets on the newts from the successfully updated information
for (const result of resourcesResults) {
for (const result of proxyResourcesResults) {
for (const target of result.targetsToUpdate) {
const [site] = await db
.select()
@@ -52,15 +69,50 @@ export async function applyBlueprint(
`Updating target ${target.targetId} on site ${site.sites.siteId}`
);
await addTargets(
await addProxyTargets(
site.newt.newtId,
[target],
result.resource.protocol,
result.resource.proxyPort
result.proxyResource.protocol,
result.proxyResource.proxyPort
);
}
}
}
logger.debug(
`Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}`
);
// We need to update the targets on the newts from the successfully updated information
for (const result of clientResourcesResults) {
const [site] = await db
.select()
.from(sites)
.innerJoin(newts, eq(sites.siteId, newts.siteId))
.where(
and(
eq(sites.siteId, result.resource.siteId),
eq(sites.orgId, orgId),
eq(sites.type, "newt"),
isNotNull(sites.pubKey)
)
)
.limit(1);
if (site) {
logger.debug(
`Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}`
);
await addClientTargets(
site.newt.newtId,
result.resource.destinationIp,
result.resource.destinationPort,
result.resource.protocol,
result.resource.proxyPort
);
}
}
} catch (error) {
logger.error(`Failed to update database from config: ${error}`);
throw error;
@@ -102,17 +154,17 @@ export async function applyBlueprint(
// }
// ]
// },
// "resource-nice-id2": {
// name: "http server",
// protocol: "tcp",
// "proxy-port": 3000,
// targets: [
// {
// site: "glossy-plains-viscacha-rat",
// hostname: "localhost",
// port: 3000,
// }
// ]
// }
// "resource-nice-id2": {
// name: "http server",
// protocol: "tcp",
// "proxy-port": 3000,
// targets: [
// {
// site: "glossy-plains-viscacha-rat",
// hostname: "localhost",
// port: 3000,
// }
// ]
// }
// }
// });

View File

@@ -0,0 +1,117 @@
import {
SiteResource,
siteResources,
Transaction,
} from "@server/db";
import { sites } from "@server/db";
import { eq, and } from "drizzle-orm";
import {
Config,
} from "./types";
import logger from "@server/logger";
export type ClientResourcesResults = {
resource: SiteResource;
}[];
export async function updateClientResources(
orgId: string,
config: Config,
trx: Transaction,
siteId?: number
): Promise<ClientResourcesResults> {
const results: ClientResourcesResults = [];
for (const [resourceNiceId, resourceData] of Object.entries(
config["client-resources"]
)) {
const [existingResource] = await trx
.select()
.from(siteResources)
.where(
and(
eq(siteResources.orgId, orgId),
eq(siteResources.niceId, resourceNiceId)
)
)
.limit(1);
const resourceSiteId = resourceData.site;
let site;
if (resourceSiteId) {
// Look up site by niceId
[site] = await trx
.select({ siteId: sites.siteId })
.from(sites)
.where(
and(
eq(sites.niceId, resourceSiteId),
eq(sites.orgId, orgId)
)
)
.limit(1);
} else if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org
[site] = await trx
.select({ siteId: sites.siteId })
.from(sites)
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
.limit(1);
} else {
throw new Error(`Target site is required`);
}
if (!site) {
throw new Error(
`Site not found: ${resourceSiteId} in org ${orgId}`
);
}
if (existingResource) {
// Update existing resource
const [updatedResource] = await trx
.update(siteResources)
.set({
name: resourceData.name || resourceNiceId,
siteId: site.siteId,
proxyPort: resourceData["proxy-port"]!,
destinationIp: resourceData.hostname,
destinationPort: resourceData["internal-port"],
protocol: resourceData.protocol
})
.where(
eq(
siteResources.siteResourceId,
existingResource.siteResourceId
)
)
.returning();
results.push({ resource: updatedResource });
} else {
// Create new resource
const [newResource] = await trx
.insert(siteResources)
.values({
orgId: orgId,
siteId: site.siteId,
niceId: resourceNiceId,
name: resourceData.name || resourceNiceId,
proxyPort: resourceData["proxy-port"]!,
destinationIp: resourceData.hostname,
destinationPort: resourceData["internal-port"],
protocol: resourceData.protocol
})
.returning();
logger.info(
`Created new client resource ${newResource.name} (${newResource.siteResourceId}) for org ${orgId}`
);
results.push({ resource: newResource });
}
}
return results;
}

View File

@@ -66,9 +66,9 @@ export function processContainerLabels(containers: Container[]): {
const resourceLabels: DockerLabels = {};
// Filter labels that start with "pangolin.resources."
// Filter labels that start with "pangolin.proxy-resources."
Object.entries(container.labels).forEach(([key, value]) => {
if (key.startsWith("pangolin.resources.")) {
if (key.startsWith("pangolin.proxy-resources.") || key.startsWith("pangolin.client-resources.")) {
// remove the pangolin. prefix
const strippedKey = key.replace("pangolin.", "");
resourceLabels[strippedKey] = value;

View File

@@ -3,6 +3,7 @@ import {
orgDomains,
Resource,
resourcePincode,
resourceRules,
resourceWhitelist,
roleResources,
roles,
@@ -14,27 +15,33 @@ import {
} from "@server/db";
import { resources, targets, sites } from "@server/db";
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
import { Config, ConfigSchema, isTargetsOnlyResource, TargetData } from "./types";
import {
Config,
ConfigSchema,
isTargetsOnlyResource,
TargetData
} from "./types";
import logger from "@server/logger";
import { pickPort } from "@server/routers/target/helpers";
import { resourcePassword } from "@server/db";
import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
export type ResourcesResults = {
resource: Resource;
export type ProxyResourcesResults = {
proxyResource: Resource;
targetsToUpdate: Target[];
}[];
export async function updateResources(
export async function updateProxyResources(
orgId: string,
config: Config,
trx: Transaction,
siteId?: number
): Promise<ResourcesResults> {
const results: ResourcesResults = [];
): Promise<ProxyResourcesResults> {
const results: ProxyResourcesResults = [];
for (const [resourceNiceId, resourceData] of Object.entries(
config.resources
config["proxy-resources"]
)) {
const targetsToUpdate: Target[] = [];
let resource: Resource;
@@ -122,8 +129,14 @@ export async function updateResources(
const http = resourceData.protocol == "http";
const protocol =
resourceData.protocol == "http" ? "tcp" : resourceData.protocol;
const resourceEnabled = resourceData.enabled == undefined || resourceData.enabled == null ? true : resourceData.enabled;
const resourceSsl = resourceData.ssl == undefined || resourceData.ssl == null ? true : resourceData.ssl;
const resourceEnabled =
resourceData.enabled == undefined || resourceData.enabled == null
? true
: resourceData.enabled;
const resourceSsl =
resourceData.ssl == undefined || resourceData.ssl == null
? true
: resourceData.ssl;
let headers = "";
for (const headerObj of resourceData.headers || []) {
for (const [key, value] of Object.entries(headerObj)) {
@@ -147,9 +160,7 @@ export async function updateResources(
}
// check if the only key in the resource is targets, if so, skip the update
if (
isTargetsOnlyResource(resourceData)
) {
if (isTargetsOnlyResource(resourceData)) {
logger.debug(
`Skipping update for resource ${existingResource.resourceId} as only targets are provided`
);
@@ -177,6 +188,8 @@ export async function updateResources(
? resourceData.auth["whitelist-users"].length > 0
: false,
headers: headers || null,
applyRules:
resourceData.rules && resourceData.rules.length > 0
})
.where(
eq(resources.resourceId, existingResource.resourceId)
@@ -262,7 +275,11 @@ export async function updateResources(
// Create new targets
for (const [index, targetData] of resourceData.targets.entries()) {
if (!targetData || (typeof targetData === 'object' && Object.keys(targetData).length === 0)) {
if (
!targetData ||
(typeof targetData === "object" &&
Object.keys(targetData).length === 0)
) {
// If targetData is null or an empty object, we can skip it
continue;
}
@@ -354,19 +371,65 @@ export async function updateResources(
const targetsToDelete = existingResourceTargets.slice(
resourceData.targets.length
);
logger.debug(`Targets to delete: ${JSON.stringify(targetsToDelete)}`);
logger.debug(
`Targets to delete: ${JSON.stringify(targetsToDelete)}`
);
for (const target of targetsToDelete) {
if (!target) {
continue;
}
}
if (siteId && target.siteId !== siteId) {
logger.debug(`Skipping target ${target.targetId} for deletion. Site ID does not match filter.`);
logger.debug(
`Skipping target ${target.targetId} for deletion. Site ID does not match filter.`
);
continue; // only delete targets for the specified siteId
}
logger.debug(`Deleting target ${target.targetId}`);
await trx
.delete(targets)
.where(eq(targets.targetId, target.targetId));
.where(eq(targets.targetId, target.targetId));
}
}
const existingRules = await trx
.select()
.from(resourceRules)
.where(
eq(resourceRules.resourceId, existingResource.resourceId)
)
.orderBy(resourceRules.priority);
// Sync rules
for (const [index, rule] of resourceData.rules?.entries() || []) {
const existingRule = existingRules[index];
if (existingRule) {
if (
existingRule.action !== rule.action ||
existingRule.match !== rule.match ||
existingRule.value !== rule.value
) {
await trx
.update(resourceRules)
.set({
action: rule.action,
match: rule.match,
value: rule.value
})
.where(
eq(resourceRules.ruleId, existingRule.ruleId)
);
}
}
}
if (existingRules.length > (resourceData.rules?.length || 0)) {
const rulesToDelete = existingRules.slice(
resourceData.rules?.length || 0
);
for (const rule of rulesToDelete) {
await trx
.delete(resourceRules)
.where(eq(resourceRules.ruleId, rule.ruleId));
}
}
@@ -401,7 +464,9 @@ export async function updateResources(
setHostHeader: resourceData["host-header"] || null,
tlsServerName: resourceData["tls-server-name"] || null,
ssl: resourceSsl,
headers: headers || null
headers: headers || null,
applyRules:
resourceData.rules && resourceData.rules.length > 0
})
.returning();
@@ -484,12 +549,37 @@ export async function updateResources(
await createTarget(newResource.resourceId, targetData);
}
for (const [index, rule] of resourceData.rules?.entries() || []) {
if (rule.match === "cidr") {
if (!isValidCIDR(rule.value)) {
throw new Error(`Invalid CIDR provided: ${rule.value}`);
}
} else if (rule.match === "ip") {
if (!isValidIP(rule.value)) {
throw new Error(`Invalid IP provided: ${rule.value}`);
}
} else if (rule.match === "path") {
if (!isValidUrlGlobPattern(rule.value)) {
throw new Error(
`Invalid URL glob pattern: ${rule.value}`
);
}
}
await trx.insert(resourceRules).values({
resourceId: newResource.resourceId,
action: rule.action,
match: rule.match,
value: rule.value,
priority: index + 1 // start priorities at 1
});
}
logger.debug(`Created resource ${newResource.resourceId}`);
}
results.push({
resource: resource,
targetsToUpdate,
proxyResource: resource,
targetsToUpdate
});
}

View File

@@ -31,7 +31,13 @@ export const AuthSchema = z.object({
message: "Admin role cannot be included in sso-roles"
}),
"sso-users": z.array(z.string().email()).optional().default([]),
"whitelist-users": z.array(z.string().email()).optional().default([])
"whitelist-users": z.array(z.string().email()).optional().default([]),
});
export const RuleSchema = z.object({
action: z.enum(["allow", "deny", "pass"]),
match: z.enum(["cidr", "path", "ip", "country"]),
value: z.string()
});
// Schema for individual resource
@@ -48,6 +54,7 @@ export const ResourceSchema = z
"host-header": z.string().optional(),
"tls-server-name": z.string().optional(),
headers: z.array(z.record(z.string(), z.string())).optional().default([]),
rules: z.array(RuleSchema).optional().default([]),
})
.refine(
(resource) => {
@@ -164,10 +171,21 @@ export function isTargetsOnlyResource(resource: any): boolean {
return Object.keys(resource).length === 1 && resource.targets;
}
export const ClientResourceSchema = z.object({
name: z.string().min(2).max(100),
site: z.string().min(2).max(100),
protocol: z.enum(["tcp", "udp"]),
"proxy-port": z.number().min(1).max(65535),
"hostname": z.string().min(1).max(255),
"internal-port": z.number().min(1).max(65535),
enabled: z.boolean().optional().default(true)
});
// Schema for the entire configuration object
export const ConfigSchema = z
.object({
resources: z.record(z.string(), ResourceSchema).optional().default({}),
"proxy-resources": z.record(z.string(), ResourceSchema).optional().default({}),
"client-resources": z.record(z.string(), ClientResourceSchema).optional().default({}),
sites: z.record(z.string(), SiteSchema).optional().default({})
})
.refine(
@@ -176,7 +194,7 @@ export const ConfigSchema = z
// Extract all full-domain values with their resource keys
const fullDomainMap = new Map<string, string[]>();
Object.entries(config.resources).forEach(
Object.entries(config["proxy-resources"]).forEach(
([resourceKey, resource]) => {
const fullDomain = resource["full-domain"];
if (fullDomain) {
@@ -200,7 +218,7 @@ export const ConfigSchema = z
// Extract duplicates for error message
const fullDomainMap = new Map<string, string[]>();
Object.entries(config.resources).forEach(
Object.entries(config["proxy-resources"]).forEach(
([resourceKey, resource]) => {
const fullDomain = resource["full-domain"];
if (fullDomain) {
@@ -226,6 +244,114 @@ export const ConfigSchema = z
path: ["resources"]
};
}
)
.refine(
// Enforce proxy-port uniqueness within proxy-resources
(config) => {
const proxyPortMap = new Map<number, string[]>();
Object.entries(config["proxy-resources"]).forEach(
([resourceKey, resource]) => {
const proxyPort = resource["proxy-port"];
if (proxyPort !== undefined) {
if (!proxyPortMap.has(proxyPort)) {
proxyPortMap.set(proxyPort, []);
}
proxyPortMap.get(proxyPort)!.push(resourceKey);
}
}
);
// Find duplicates
const duplicates = Array.from(proxyPortMap.entries()).filter(
([_, resourceKeys]) => resourceKeys.length > 1
);
return duplicates.length === 0;
},
(config) => {
// Extract duplicates for error message
const proxyPortMap = new Map<number, string[]>();
Object.entries(config["proxy-resources"]).forEach(
([resourceKey, resource]) => {
const proxyPort = resource["proxy-port"];
if (proxyPort !== undefined) {
if (!proxyPortMap.has(proxyPort)) {
proxyPortMap.set(proxyPort, []);
}
proxyPortMap.get(proxyPort)!.push(resourceKey);
}
}
);
const duplicates = Array.from(proxyPortMap.entries())
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
.map(
([proxyPort, resourceKeys]) =>
`port ${proxyPort} used by proxy-resources: ${resourceKeys.join(", ")}`
)
.join("; ");
return {
message: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}`,
path: ["proxy-resources"]
};
}
)
.refine(
// Enforce proxy-port uniqueness within client-resources
(config) => {
const proxyPortMap = new Map<number, string[]>();
Object.entries(config["client-resources"]).forEach(
([resourceKey, resource]) => {
const proxyPort = resource["proxy-port"];
if (proxyPort !== undefined) {
if (!proxyPortMap.has(proxyPort)) {
proxyPortMap.set(proxyPort, []);
}
proxyPortMap.get(proxyPort)!.push(resourceKey);
}
}
);
// Find duplicates
const duplicates = Array.from(proxyPortMap.entries()).filter(
([_, resourceKeys]) => resourceKeys.length > 1
);
return duplicates.length === 0;
},
(config) => {
// Extract duplicates for error message
const proxyPortMap = new Map<number, string[]>();
Object.entries(config["client-resources"]).forEach(
([resourceKey, resource]) => {
const proxyPort = resource["proxy-port"];
if (proxyPort !== undefined) {
if (!proxyPortMap.has(proxyPort)) {
proxyPortMap.set(proxyPort, []);
}
proxyPortMap.get(proxyPort)!.push(resourceKey);
}
}
);
const duplicates = Array.from(proxyPortMap.entries())
.filter(([_, resourceKeys]) => resourceKeys.length > 1)
.map(
([proxyPort, resourceKeys]) =>
`port ${proxyPort} used by client-resources: ${resourceKeys.join(", ")}`
)
.join("; ");
return {
message: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}`,
path: ["client-resources"]
};
}
);
// Type inference from the schema

View File

@@ -10,6 +10,7 @@ import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
import { addTargets } from "../client/targets";
import { getUniqueSiteResourceName } from "@server/db/names";
const createSiteResourceParamsSchema = z
.object({
@@ -121,11 +122,14 @@ export async function createSiteResource(
);
}
const niceId = await getUniqueSiteResourceName(orgId);
// Create the site resource
const [newSiteResource] = await db
.insert(siteResources)
.values({
siteId,
niceId,
orgId,
name,
protocol,

View File

@@ -12,21 +12,72 @@ import { OpenAPITags, registry } from "@server/openApi";
const getSiteResourceParamsSchema = z
.object({
siteResourceId: z.string().transform(Number).pipe(z.number().int().positive()),
siteResourceId: z
.string()
.optional()
.transform((val) => val ? Number(val) : undefined)
.pipe(z.number().int().positive().optional())
.optional(),
siteId: z.string().transform(Number).pipe(z.number().int().positive()),
niceId: z.string().optional(),
orgId: z.string()
})
.strict();
export type GetSiteResourceResponse = SiteResource;
async function query(siteResourceId?: number, siteId?: number, niceId?: string, orgId?: string) {
if (siteResourceId && siteId && orgId) {
const [siteResource] = await db
.select()
.from(siteResources)
.where(and(
eq(siteResources.siteResourceId, siteResourceId),
eq(siteResources.siteId, siteId),
eq(siteResources.orgId, orgId)
))
.limit(1);
return siteResource;
} else if (niceId && siteId && orgId) {
const [siteResource] = await db
.select()
.from(siteResources)
.where(and(
eq(siteResources.niceId, niceId),
eq(siteResources.siteId, siteId),
eq(siteResources.orgId, orgId)
))
.limit(1);
return siteResource;
}
}
export type GetSiteResourceResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
registry.registerPath({
method: "get",
path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}",
description: "Get a specific site resource.",
description: "Get a specific site resource by siteResourceId.",
tags: [OpenAPITags.Client, OpenAPITags.Org],
request: {
params: getSiteResourceParamsSchema
params: z.object({
siteResourceId: z.number(),
siteId: z.number(),
orgId: z.string()
})
},
responses: {}
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/site/{siteId}/resource/nice/{niceId}",
description: "Get a specific site resource by niceId.",
tags: [OpenAPITags.Client, OpenAPITags.Org],
request: {
params: z.object({
niceId: z.string(),
siteId: z.number(),
orgId: z.string()
})
},
responses: {}
});
@@ -47,18 +98,10 @@ export async function getSiteResource(
);
}
const { siteResourceId, siteId, orgId } = parsedParams.data;
const { siteResourceId, siteId, niceId, orgId } = parsedParams.data;
// Get the site resource
const [siteResource] = await db
.select()
.from(siteResources)
.where(and(
eq(siteResources.siteResourceId, siteResourceId),
eq(siteResources.siteId, siteId),
eq(siteResources.orgId, orgId)
))
.limit(1);
const siteResource = await query(siteResourceId, siteId, niceId, orgId);
if (!siteResource) {
return next(

View File

@@ -352,7 +352,7 @@ export default function CreateInternalResourceDialog({
render={({ field }) => (
<FormItem>
<FormLabel>
{t("createInternalResourceDialogDestinationIP")}
{t("targetAddr")}
</FormLabel>
<FormControl>
<Input
@@ -373,7 +373,7 @@ export default function CreateInternalResourceDialog({
render={({ field }) => (
<FormItem>
<FormLabel>
{t("createInternalResourceDialogDestinationPort")}
{t("targetPort")}
</FormLabel>
<FormControl>
<Input

View File

@@ -221,7 +221,7 @@ export default function EditInternalResourceDialog({
name="destinationIp"
render={({ field }) => (
<FormItem>
<FormLabel>{t("editInternalResourceDialogDestinationIP")}</FormLabel>
<FormLabel>{t("targetAddr")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -235,7 +235,7 @@ export default function EditInternalResourceDialog({
name="destinationPort"
render={({ field }) => (
<FormItem>
<FormLabel>{t("editInternalResourceDialogDestinationPort")}</FormLabel>
<FormLabel>{t("targetPort")}</FormLabel>
<FormControl>
<Input
type="number"