mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-25 13:35:32 +02:00
Remove browser gateway targets for regular targets
This commit is contained in:
@@ -580,24 +580,6 @@ export const trialNotifications = pgTable("trialNotifications", {
|
|||||||
sentAt: bigint("sentAt", { mode: "number" }).notNull()
|
sentAt: bigint("sentAt", { mode: "number" }).notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const browserGatewayTarget = pgTable("browserGatewayTarget", {
|
|
||||||
browserGatewayTargetId: serial("browserGatewayTargetId").primaryKey(),
|
|
||||||
resourceId: integer("resourceId")
|
|
||||||
.references(() => resources.resourceId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
})
|
|
||||||
.notNull(),
|
|
||||||
siteId: integer("siteId")
|
|
||||||
.references(() => sites.siteId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
})
|
|
||||||
.notNull(),
|
|
||||||
authToken: varchar("authToken").notNull(),
|
|
||||||
type: varchar("type").notNull(), // "ssh", "rdp", "vnc"
|
|
||||||
destination: varchar("destination").notNull(),
|
|
||||||
destinationPort: integer("destinationPort").notNull()
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
@@ -645,6 +627,3 @@ export type AlertEmailRecipients = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type AlertWebhookActions = InferSelectModel<typeof alertWebhookActions>;
|
export type AlertWebhookActions = InferSelectModel<typeof alertWebhookActions>;
|
||||||
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
|
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
|
||||||
export type BrowserGatewayTarget = InferSelectModel<
|
|
||||||
typeof browserGatewayTarget
|
|
||||||
>;
|
|
||||||
|
|||||||
@@ -290,7 +290,12 @@ export const targets = pgTable("targets", {
|
|||||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
||||||
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
|
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
|
||||||
priority: integer("priority").notNull().default(100)
|
priority: integer("priority").notNull().default(100),
|
||||||
|
mode: varchar("mode")
|
||||||
|
.$type<"http" | "tcp" | "udp" | "ssh" | "rdp" | "vnc">()
|
||||||
|
.notNull()
|
||||||
|
.default("http"),
|
||||||
|
authToken: varchar("authToken")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||||
|
|||||||
@@ -588,26 +588,6 @@ export const trialNotifications = sqliteTable("trialNotifications", {
|
|||||||
sentAt: integer("sentAt").notNull()
|
sentAt: integer("sentAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const browserGatewayTarget = sqliteTable("browserGatewayTarget", {
|
|
||||||
browserGatewayTargetId: integer("browserGatewayTargetId").primaryKey({
|
|
||||||
autoIncrement: true
|
|
||||||
}),
|
|
||||||
resourceId: integer("resourceId")
|
|
||||||
.references(() => resources.resourceId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
})
|
|
||||||
.notNull(),
|
|
||||||
siteId: integer("siteId")
|
|
||||||
.references(() => sites.siteId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
})
|
|
||||||
.notNull(),
|
|
||||||
authToken: text("authToken").notNull(),
|
|
||||||
type: text("type").notNull(), // "ssh", "rdp", "vnc"
|
|
||||||
destination: text("destination").notNull(),
|
|
||||||
destinationPort: integer("destinationPort").notNull()
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
@@ -647,6 +627,3 @@ export type AlertEmailAction = InferSelectModel<typeof alertEmailActions>;
|
|||||||
export type AlertEmailRecipient = InferSelectModel<typeof alertEmailRecipients>;
|
export type AlertEmailRecipient = InferSelectModel<typeof alertEmailRecipients>;
|
||||||
export type AlertWebhookAction = InferSelectModel<typeof alertWebhookActions>;
|
export type AlertWebhookAction = InferSelectModel<typeof alertWebhookActions>;
|
||||||
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
|
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
|
||||||
export type BrowserGatewayTarget = InferSelectModel<
|
|
||||||
typeof browserGatewayTarget
|
|
||||||
>;
|
|
||||||
|
|||||||
@@ -322,7 +322,12 @@ export const targets = sqliteTable("targets", {
|
|||||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
||||||
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
|
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
|
||||||
priority: integer("priority").notNull().default(100)
|
priority: integer("priority").notNull().default(100),
|
||||||
|
mode: text("mode")
|
||||||
|
.$type<"http" | "tcp" | "udp" | "ssh" | "rdp" | "vnc">()
|
||||||
|
.notNull()
|
||||||
|
.default("http"),
|
||||||
|
authToken: text("authToken")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
browserGatewayTarget,
|
|
||||||
certificates,
|
certificates,
|
||||||
db,
|
db,
|
||||||
domainNamespaces,
|
domainNamespaces,
|
||||||
@@ -182,6 +181,9 @@ export async function getTraefikConfig(
|
|||||||
const resourcesMap = new Map();
|
const resourcesMap = new Map();
|
||||||
|
|
||||||
resourcesWithTargetsAndSites.forEach((row) => {
|
resourcesWithTargetsAndSites.forEach((row) => {
|
||||||
|
if (!["http", "tcp", "udp"].includes(row.mode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const resourceId = row.resourceId;
|
const resourceId = row.resourceId;
|
||||||
const resourceName = sanitize(row.resourceName) || "";
|
const resourceName = sanitize(row.resourceName) || "";
|
||||||
const targetPath = encodePath(row.path); // Use encodePath to avoid collisions (e.g. "/a/b" vs "/a-b")
|
const targetPath = encodePath(row.path); // Use encodePath to avoid collisions (e.g. "/a/b" vs "/a-b")
|
||||||
@@ -295,13 +297,12 @@ export async function getTraefikConfig(
|
|||||||
maintenanceMessage: string | null;
|
maintenanceMessage: string | null;
|
||||||
maintenanceEstimatedTime: string | null;
|
maintenanceEstimatedTime: string | null;
|
||||||
targets: {
|
targets: {
|
||||||
browserGatewayTargetId: number;
|
targetId: number;
|
||||||
bgType: string;
|
bgType: string;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
siteType: string;
|
siteType: string;
|
||||||
siteOnline: boolean | null;
|
siteOnline: boolean | null;
|
||||||
subnet: string | null;
|
subnet: string | null;
|
||||||
siteExitNodeId: number | null;
|
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
const browserGatewayResourcesMap = new Map<
|
const browserGatewayResourcesMap = new Map<
|
||||||
@@ -310,66 +311,10 @@ export async function getTraefikConfig(
|
|||||||
>();
|
>();
|
||||||
|
|
||||||
if (allowBrowserGatewayResources) {
|
if (allowBrowserGatewayResources) {
|
||||||
// Query browser gateway targets for this exit node
|
for (const row of resourcesWithTargetsAndSites) {
|
||||||
const browserGatewayRows = await db
|
if (!["ssh", "vnc", "rdp"].includes(row.mode)) {
|
||||||
.select({
|
return;
|
||||||
// Resource fields
|
}
|
||||||
resourceId: resources.resourceId,
|
|
||||||
resourceName: resources.name,
|
|
||||||
fullDomain: resources.fullDomain,
|
|
||||||
ssl: resources.ssl,
|
|
||||||
subdomain: resources.subdomain,
|
|
||||||
domainId: resources.domainId,
|
|
||||||
enabled: resources.enabled,
|
|
||||||
wildcard: resources.wildcard,
|
|
||||||
domainCertResolver: domains.certResolver,
|
|
||||||
preferWildcardCert: domains.preferWildcardCert,
|
|
||||||
domainNamespaceId: domainNamespaces.domainNamespaceId,
|
|
||||||
// Maintenance fields
|
|
||||||
maintenanceModeEnabled: resources.maintenanceModeEnabled,
|
|
||||||
maintenanceModeType: resources.maintenanceModeType,
|
|
||||||
maintenanceTitle: resources.maintenanceTitle,
|
|
||||||
maintenanceMessage: resources.maintenanceMessage,
|
|
||||||
maintenanceEstimatedTime: resources.maintenanceEstimatedTime,
|
|
||||||
// Browser gateway target fields
|
|
||||||
browserGatewayTargetId:
|
|
||||||
browserGatewayTarget.browserGatewayTargetId,
|
|
||||||
bgType: browserGatewayTarget.type,
|
|
||||||
// Site fields
|
|
||||||
siteId: sites.siteId,
|
|
||||||
siteType: sites.type,
|
|
||||||
siteOnline: sites.online,
|
|
||||||
subnet: sites.subnet,
|
|
||||||
siteExitNodeId: sites.exitNodeId
|
|
||||||
})
|
|
||||||
.from(browserGatewayTarget)
|
|
||||||
.innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
|
|
||||||
.innerJoin(
|
|
||||||
resources,
|
|
||||||
eq(resources.resourceId, browserGatewayTarget.resourceId)
|
|
||||||
)
|
|
||||||
.leftJoin(domains, eq(domains.domainId, resources.domainId))
|
|
||||||
.leftJoin(
|
|
||||||
domainNamespaces,
|
|
||||||
eq(domainNamespaces.domainId, resources.domainId)
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(resources.enabled, true),
|
|
||||||
or(
|
|
||||||
eq(sites.exitNodeId, exitNodeId),
|
|
||||||
and(
|
|
||||||
isNull(sites.exitNodeId),
|
|
||||||
sql`(${siteTypes.includes("local") ? 1 : 0} = 1)`,
|
|
||||||
eq(sites.type, "local"),
|
|
||||||
sql`(${build != "saas" ? 1 : 0} = 1)`
|
|
||||||
)
|
|
||||||
),
|
|
||||||
inArray(sites.type, siteTypes)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const row of browserGatewayRows) {
|
|
||||||
if (filterOutNamespaceDomains && row.domainNamespaceId) {
|
if (filterOutNamespaceDomains && row.domainNamespaceId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -394,13 +339,12 @@ export async function getTraefikConfig(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
browserGatewayResourcesMap.get(row.resourceId)!.targets.push({
|
browserGatewayResourcesMap.get(row.resourceId)!.targets.push({
|
||||||
browserGatewayTargetId: row.browserGatewayTargetId,
|
targetId: row.targetId,
|
||||||
bgType: row.bgType,
|
bgType: row.mode,
|
||||||
siteId: row.siteId,
|
siteId: row.siteId,
|
||||||
siteType: row.siteType,
|
siteType: row.siteType,
|
||||||
siteOnline: row.siteOnline,
|
siteOnline: row.siteOnline,
|
||||||
subnet: row.subnet,
|
subnet: row.subnet
|
||||||
siteExitNodeId: row.siteExitNodeId
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,187 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
browserGatewayTarget,
|
|
||||||
BrowserGatewayTarget,
|
|
||||||
db,
|
|
||||||
newts,
|
|
||||||
resources,
|
|
||||||
sites
|
|
||||||
} from "@server/db";
|
|
||||||
import { eq, and } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
import { encrypt } from "@server/lib/crypto";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import { sendBrowserGatewayTargets } from "@server/routers/newt/targets";
|
|
||||||
import { generateId } from "@server/auth/sessions/app";
|
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
|
||||||
orgId: z.string().nonempty(),
|
|
||||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive())
|
|
||||||
});
|
|
||||||
|
|
||||||
const bodySchema = z.strictObject({
|
|
||||||
siteId: z.number().int().positive(),
|
|
||||||
type: z.enum(["ssh", "rdp", "vnc"]),
|
|
||||||
destination: z.string().nonempty(),
|
|
||||||
destinationPort: z.number().int().min(1).max(65535)
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CreateBrowserGatewayTargetResponse = BrowserGatewayTarget;
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "put",
|
|
||||||
path: "/org/{orgId}/resource/{resourceId}/browser-gateway-target",
|
|
||||||
description: "Create a browser gateway target for a resource.",
|
|
||||||
tags: [OpenAPITags.Org],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema,
|
|
||||||
body: {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: bodySchema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function createBrowserGatewayTarget(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { orgId, resourceId } = parsedParams.data;
|
|
||||||
|
|
||||||
const parsedBody = bodySchema.safeParse(req.body);
|
|
||||||
if (!parsedBody.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedBody.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { siteId, type, destination, destinationPort } = parsedBody.data;
|
|
||||||
|
|
||||||
const [resource] = await db
|
|
||||||
.select()
|
|
||||||
.from(resources)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(resources.resourceId, resourceId),
|
|
||||||
eq(resources.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Resource with ID ${resourceId} not found in organization ${orgId}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [site] = await db
|
|
||||||
.select()
|
|
||||||
.from(sites)
|
|
||||||
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!site) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Site with ID ${siteId} not found in organization ${orgId}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const plainToken = generateId(48);
|
|
||||||
const encryptedToken = encrypt(
|
|
||||||
plainToken,
|
|
||||||
config.getRawConfig().server.secret!
|
|
||||||
);
|
|
||||||
|
|
||||||
const [record] = await db
|
|
||||||
.insert(browserGatewayTarget)
|
|
||||||
.values({
|
|
||||||
resourceId,
|
|
||||||
siteId,
|
|
||||||
type,
|
|
||||||
destination,
|
|
||||||
destinationPort,
|
|
||||||
authToken: encryptedToken
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (site.type === "newt") {
|
|
||||||
const [newt] = await db
|
|
||||||
.select()
|
|
||||||
.from(newts)
|
|
||||||
.where(eq(newts.siteId, siteId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (newt) {
|
|
||||||
await sendBrowserGatewayTargets(
|
|
||||||
newt.newtId,
|
|
||||||
[record],
|
|
||||||
newt.version
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Created browser gateway target ${record.browserGatewayTargetId} for resource ${resourceId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return response<CreateBrowserGatewayTargetResponse>(res, {
|
|
||||||
data: record,
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "Browser gateway target created successfully",
|
|
||||||
status: HttpCode.CREATED
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to create browser gateway target"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { browserGatewayTarget, db, newts, sites } from "@server/db";
|
|
||||||
import { eq, and } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
import { removeBrowserGatewayTarget } from "@server/routers/newt/targets";
|
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
|
||||||
orgId: z.string().nonempty(),
|
|
||||||
browserGatewayTargetId: z
|
|
||||||
.string()
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().positive())
|
|
||||||
});
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "delete",
|
|
||||||
path: "/org/{orgId}/browser-gateway-target/{browserGatewayTargetId}",
|
|
||||||
description: "Delete a browser gateway target.",
|
|
||||||
tags: [OpenAPITags.Org],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function deleteBrowserGatewayTarget(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { orgId, browserGatewayTargetId } = parsedParams.data;
|
|
||||||
|
|
||||||
const [existing] = await db
|
|
||||||
.select({ bgt: browserGatewayTarget, site: sites })
|
|
||||||
.from(browserGatewayTarget)
|
|
||||||
.innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(
|
|
||||||
browserGatewayTarget.browserGatewayTargetId,
|
|
||||||
browserGatewayTargetId
|
|
||||||
),
|
|
||||||
eq(sites.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!existing) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Browser gateway target with ID ${browserGatewayTargetId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.delete(browserGatewayTarget)
|
|
||||||
.where(
|
|
||||||
eq(
|
|
||||||
browserGatewayTarget.browserGatewayTargetId,
|
|
||||||
browserGatewayTargetId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existing.site.type === "newt") {
|
|
||||||
const [newt] = await db
|
|
||||||
.select()
|
|
||||||
.from(newts)
|
|
||||||
.where(eq(newts.siteId, existing.bgt.siteId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (newt) {
|
|
||||||
await removeBrowserGatewayTarget(
|
|
||||||
newt.newtId,
|
|
||||||
browserGatewayTargetId,
|
|
||||||
newt.version
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Deleted browser gateway target ${browserGatewayTargetId}`);
|
|
||||||
|
|
||||||
return response(res, {
|
|
||||||
data: null,
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "Browser gateway target deleted successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to delete browser gateway target"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
browserGatewayTarget,
|
|
||||||
BrowserGatewayTarget,
|
|
||||||
db,
|
|
||||||
sites
|
|
||||||
} from "@server/db";
|
|
||||||
import { eq, and } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
|
||||||
orgId: z.string().nonempty(),
|
|
||||||
browserGatewayTargetId: z
|
|
||||||
.string()
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().positive())
|
|
||||||
});
|
|
||||||
|
|
||||||
export type GetBrowserGatewayTargetResponse = BrowserGatewayTarget;
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "get",
|
|
||||||
path: "/org/{orgId}/browser-gateway-target/{browserGatewayTargetId}",
|
|
||||||
description: "Get a browser gateway target.",
|
|
||||||
tags: [OpenAPITags.Org],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function getBrowserGatewayTarget(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { orgId, browserGatewayTargetId } = parsedParams.data;
|
|
||||||
|
|
||||||
const [result] = await db
|
|
||||||
.select({ bgt: browserGatewayTarget })
|
|
||||||
.from(browserGatewayTarget)
|
|
||||||
.innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(
|
|
||||||
browserGatewayTarget.browserGatewayTargetId,
|
|
||||||
browserGatewayTargetId
|
|
||||||
),
|
|
||||||
eq(sites.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Browser gateway target with ID ${browserGatewayTargetId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response<GetBrowserGatewayTargetResponse>(res, {
|
|
||||||
data: result.bgt,
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "Browser gateway target retrieved successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to retrieve browser gateway target"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,9 +13,8 @@
|
|||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { browserGatewayTarget, db } from "@server/db";
|
import { db, resources, targets } from "@server/db";
|
||||||
import { resources, targets } from "@server/db";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -51,11 +50,11 @@ export async function getBrowserTarget(
|
|||||||
|
|
||||||
logger.info(`Retrieving browser target for domain: ${fullDomain}`);
|
logger.info(`Retrieving browser target for domain: ${fullDomain}`);
|
||||||
|
|
||||||
const [browserTarget] = await db
|
const [row] = await db
|
||||||
.select({
|
.select({
|
||||||
destination: browserGatewayTarget.destination,
|
ip: targets.ip,
|
||||||
destinationPort: browserGatewayTarget.destinationPort,
|
port: targets.port,
|
||||||
authToken: browserGatewayTarget.authToken,
|
authToken: targets.authToken,
|
||||||
resourceId: resources.resourceId,
|
resourceId: resources.resourceId,
|
||||||
niceId: resources.niceId,
|
niceId: resources.niceId,
|
||||||
name: resources.name,
|
name: resources.name,
|
||||||
@@ -63,20 +62,18 @@ export async function getBrowserTarget(
|
|||||||
pamMode: resources.pamMode,
|
pamMode: resources.pamMode,
|
||||||
authDaemonMode: resources.authDaemonMode
|
authDaemonMode: resources.authDaemonMode
|
||||||
})
|
})
|
||||||
.from(browserGatewayTarget)
|
.from(targets)
|
||||||
.innerJoin(
|
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||||
resources,
|
.where(
|
||||||
eq(browserGatewayTarget.resourceId, resources.resourceId)
|
and(
|
||||||
|
eq(resources.fullDomain, fullDomain),
|
||||||
|
eq(targets.enabled, true),
|
||||||
|
inArray(targets.mode, ["ssh", "rdp", "vnc"])
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.where(eq(resources.fullDomain, fullDomain))
|
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
const decryptedAuthToken = decrypt(
|
if (!row) {
|
||||||
browserTarget.authToken,
|
|
||||||
config.getRawConfig().server.secret!
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!browserTarget) {
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
HttpCode.NOT_FOUND,
|
||||||
@@ -85,17 +82,21 @@ export async function getBrowserTarget(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decryptedAuthToken = row.authToken
|
||||||
|
? decrypt(row.authToken, config.getRawConfig().server.secret!)
|
||||||
|
: "";
|
||||||
|
|
||||||
return response<GetBrowserTargetResponse>(res, {
|
return response<GetBrowserTargetResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
ip: browserTarget.destination,
|
ip: row.ip,
|
||||||
port: browserTarget.destinationPort,
|
port: row.port,
|
||||||
authToken: decryptedAuthToken,
|
authToken: decryptedAuthToken,
|
||||||
pamMode: browserTarget.pamMode,
|
pamMode: row.pamMode,
|
||||||
authDaemonMode: browserTarget.authDaemonMode,
|
authDaemonMode: row.authDaemonMode,
|
||||||
orgId: browserTarget.orgId,
|
orgId: row.orgId,
|
||||||
resourceId: browserTarget.resourceId,
|
resourceId: row.resourceId,
|
||||||
niceId: browserTarget.niceId,
|
niceId: row.niceId,
|
||||||
name: browserTarget.name
|
name: row.name ?? ""
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -11,9 +11,4 @@
|
|||||||
* This file is not licensed under the AGPLv3.
|
* This file is not licensed under the AGPLv3.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./createBrowserGatewayTarget";
|
|
||||||
export * from "./updateBrowserGatewayTarget";
|
|
||||||
export * from "./deleteBrowserGatewayTarget";
|
|
||||||
export * from "./getBrowserGatewayTarget";
|
|
||||||
export * from "./listBrowserGatewayTargets";
|
|
||||||
export * from "./getBrowserTarget";
|
export * from "./getBrowserTarget";
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
browserGatewayTarget,
|
|
||||||
BrowserGatewayTarget,
|
|
||||||
db,
|
|
||||||
resources,
|
|
||||||
sites
|
|
||||||
} from "@server/db";
|
|
||||||
import { eq, and } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
|
||||||
orgId: z.string().nonempty(),
|
|
||||||
resourceId: z.string().transform(Number).pipe(z.number().int().positive())
|
|
||||||
});
|
|
||||||
|
|
||||||
const querySchema = z.object({
|
|
||||||
limit: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("1000")
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().positive()),
|
|
||||||
offset: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("0")
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().nonnegative())
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ListBrowserGatewayTargetsResponse = {
|
|
||||||
targets: BrowserGatewayTarget[];
|
|
||||||
total: number;
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "get",
|
|
||||||
path: "/org/{orgId}/resource/{resourceId}/browser-gateway-targets",
|
|
||||||
description: "List browser gateway targets for a resource.",
|
|
||||||
tags: [OpenAPITags.Org],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema,
|
|
||||||
query: querySchema
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function listBrowserGatewayTargets(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { orgId, resourceId } = parsedParams.data;
|
|
||||||
|
|
||||||
const parsedQuery = querySchema.safeParse(req.query);
|
|
||||||
if (!parsedQuery.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedQuery.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { limit, offset } = parsedQuery.data;
|
|
||||||
|
|
||||||
const [resource] = await db
|
|
||||||
.select()
|
|
||||||
.from(resources)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(resources.resourceId, resourceId),
|
|
||||||
eq(resources.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Resource with ID ${resourceId} not found in organization ${orgId}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await db
|
|
||||||
.select({
|
|
||||||
browserGatewayTargetId:
|
|
||||||
browserGatewayTarget.browserGatewayTargetId,
|
|
||||||
resourceId: browserGatewayTarget.resourceId,
|
|
||||||
siteId: browserGatewayTarget.siteId,
|
|
||||||
authToken: browserGatewayTarget.authToken,
|
|
||||||
type: browserGatewayTarget.type,
|
|
||||||
destination: browserGatewayTarget.destination,
|
|
||||||
destinationPort: browserGatewayTarget.destinationPort,
|
|
||||||
siteName: sites.name
|
|
||||||
})
|
|
||||||
.from(browserGatewayTarget)
|
|
||||||
.leftJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
|
|
||||||
.where(eq(browserGatewayTarget.resourceId, resourceId))
|
|
||||||
.limit(limit)
|
|
||||||
.offset(offset);
|
|
||||||
|
|
||||||
return response<ListBrowserGatewayTargetsResponse>(res, {
|
|
||||||
data: {
|
|
||||||
targets: rows as any,
|
|
||||||
total: rows.length,
|
|
||||||
limit,
|
|
||||||
offset
|
|
||||||
},
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "Browser gateway targets retrieved successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to list browser gateway targets"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
browserGatewayTarget,
|
|
||||||
BrowserGatewayTarget,
|
|
||||||
db,
|
|
||||||
newts,
|
|
||||||
sites
|
|
||||||
} from "@server/db";
|
|
||||||
import { eq, and } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
import { sendBrowserGatewayTargets } from "@server/routers/newt/targets";
|
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
|
||||||
orgId: z.string().nonempty(),
|
|
||||||
browserGatewayTargetId: z
|
|
||||||
.string()
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().positive())
|
|
||||||
});
|
|
||||||
|
|
||||||
const bodySchema = z.strictObject({
|
|
||||||
siteId: z.number().int().positive().optional(),
|
|
||||||
type: z.enum(["ssh", "rdp", "vnc"]).optional(),
|
|
||||||
destination: z.string().nonempty().optional(),
|
|
||||||
destinationPort: z.number().int().min(1).max(65535).optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export type UpdateBrowserGatewayTargetResponse = BrowserGatewayTarget;
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "post",
|
|
||||||
path: "/org/{orgId}/browser-gateway-target/{browserGatewayTargetId}",
|
|
||||||
description: "Update a browser gateway target.",
|
|
||||||
tags: [OpenAPITags.Org],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema,
|
|
||||||
body: {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: bodySchema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function updateBrowserGatewayTarget(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { orgId, browserGatewayTargetId } = parsedParams.data;
|
|
||||||
|
|
||||||
const parsedBody = bodySchema.safeParse(req.body);
|
|
||||||
if (!parsedBody.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedBody.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { siteId, type, destination, destinationPort } = parsedBody.data;
|
|
||||||
|
|
||||||
const [existing] = await db
|
|
||||||
.select({ bgt: browserGatewayTarget, site: sites })
|
|
||||||
.from(browserGatewayTarget)
|
|
||||||
.innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(
|
|
||||||
browserGatewayTarget.browserGatewayTargetId,
|
|
||||||
browserGatewayTargetId
|
|
||||||
),
|
|
||||||
eq(sites.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!existing) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Browser gateway target with ID ${browserGatewayTargetId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateValues: Partial<BrowserGatewayTarget> = {};
|
|
||||||
if (siteId !== undefined) updateValues.siteId = siteId;
|
|
||||||
if (type !== undefined) updateValues.type = type;
|
|
||||||
if (destination !== undefined) updateValues.destination = destination;
|
|
||||||
if (destinationPort !== undefined)
|
|
||||||
updateValues.destinationPort = destinationPort;
|
|
||||||
|
|
||||||
const [updated] = await db
|
|
||||||
.update(browserGatewayTarget)
|
|
||||||
.set(updateValues)
|
|
||||||
.where(
|
|
||||||
eq(
|
|
||||||
browserGatewayTarget.browserGatewayTargetId,
|
|
||||||
browserGatewayTargetId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
const targetSiteId = siteId ?? existing.bgt.siteId;
|
|
||||||
const [site] = await db
|
|
||||||
.select()
|
|
||||||
.from(sites)
|
|
||||||
.where(eq(sites.siteId, targetSiteId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (site && site.type === "newt") {
|
|
||||||
const [newt] = await db
|
|
||||||
.select()
|
|
||||||
.from(newts)
|
|
||||||
.where(eq(newts.siteId, targetSiteId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (newt) {
|
|
||||||
await sendBrowserGatewayTargets(
|
|
||||||
newt.newtId,
|
|
||||||
[updated],
|
|
||||||
newt.version
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Updated browser gateway target ${browserGatewayTargetId}`);
|
|
||||||
|
|
||||||
return response<UpdateBrowserGatewayTargetResponse>(res, {
|
|
||||||
data: updated,
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "Browser gateway target updated successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to update browser gateway target"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,6 @@ import * as siteProvisioning from "#private/routers/siteProvisioning";
|
|||||||
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
||||||
import * as alertRule from "#private/routers/alertRule";
|
import * as alertRule from "#private/routers/alertRule";
|
||||||
import * as healthChecks from "#private/routers/healthChecks";
|
import * as healthChecks from "#private/routers/healthChecks";
|
||||||
import * as browserGatewayTarget from "#private/routers/browserGatewayTarget";
|
|
||||||
import * as labels from "#private/routers/labels";
|
import * as labels from "#private/routers/labels";
|
||||||
import * as client from "@server/routers/client";
|
import * as client from "@server/routers/client";
|
||||||
import * as resource from "#private/routers/resource";
|
import * as resource from "#private/routers/resource";
|
||||||
@@ -879,48 +878,3 @@ authenticated.post(
|
|||||||
verifyClientAccess,
|
verifyClientAccess,
|
||||||
client.rebuildClientAssociationsCacheRoute
|
client.rebuildClientAssociationsCacheRoute
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/org/:orgId/resource/:resourceId/browser-gateway-target",
|
|
||||||
verifyValidLicense,
|
|
||||||
verifyOrgAccess,
|
|
||||||
verifyLimits,
|
|
||||||
verifyUserHasAction(ActionsEnum.createBrowserGatewayTarget),
|
|
||||||
logActionAudit(ActionsEnum.createBrowserGatewayTarget),
|
|
||||||
browserGatewayTarget.createBrowserGatewayTarget
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/resource/:resourceId/browser-gateway-targets",
|
|
||||||
verifyValidLicense,
|
|
||||||
verifyOrgAccess,
|
|
||||||
verifyUserHasAction(ActionsEnum.listBrowserGatewayTargets),
|
|
||||||
browserGatewayTarget.listBrowserGatewayTargets
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/browser-gateway-target/:browserGatewayTargetId",
|
|
||||||
verifyValidLicense,
|
|
||||||
verifyOrgAccess,
|
|
||||||
verifyUserHasAction(ActionsEnum.getBrowserGatewayTarget),
|
|
||||||
browserGatewayTarget.getBrowserGatewayTarget
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/org/:orgId/browser-gateway-target/:browserGatewayTargetId",
|
|
||||||
verifyValidLicense,
|
|
||||||
verifyOrgAccess,
|
|
||||||
verifyLimits,
|
|
||||||
verifyUserHasAction(ActionsEnum.updateBrowserGatewayTarget),
|
|
||||||
logActionAudit(ActionsEnum.updateBrowserGatewayTarget),
|
|
||||||
browserGatewayTarget.updateBrowserGatewayTarget
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/org/:orgId/browser-gateway-target/:browserGatewayTargetId",
|
|
||||||
verifyValidLicense,
|
|
||||||
verifyOrgAccess,
|
|
||||||
verifyUserHasAction(ActionsEnum.deleteBrowserGatewayTarget),
|
|
||||||
logActionAudit(ActionsEnum.deleteBrowserGatewayTarget),
|
|
||||||
browserGatewayTarget.deleteBrowserGatewayTarget
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import * as org from "#private/routers/org";
|
|||||||
import * as logs from "#private/routers/auditLogs";
|
import * as logs from "#private/routers/auditLogs";
|
||||||
import * as alertEvents from "#private/routers/alertEvents";
|
import * as alertEvents from "#private/routers/alertEvents";
|
||||||
import * as certificates from "#private/routers/certificates";
|
import * as certificates from "#private/routers/certificates";
|
||||||
import * as browserGatewayTarget from "#private/routers/browserGatewayTarget";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifyApiKeyHasAction,
|
verifyApiKeyHasAction,
|
||||||
@@ -216,43 +215,3 @@ authenticated.delete(
|
|||||||
logActionAudit(ActionsEnum.removeUserRole),
|
logActionAudit(ActionsEnum.removeUserRole),
|
||||||
user.removeUserRole
|
user.removeUserRole
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/org/:orgId/resource/:resourceId/browser-gateway-target",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyLimits,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createBrowserGatewayTarget),
|
|
||||||
logActionAudit(ActionsEnum.createBrowserGatewayTarget),
|
|
||||||
browserGatewayTarget.createBrowserGatewayTarget
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/resource/:resourceId/browser-gateway-targets",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listBrowserGatewayTargets),
|
|
||||||
browserGatewayTarget.listBrowserGatewayTargets
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/browser-gateway-target/:browserGatewayTargetId",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.getBrowserGatewayTarget),
|
|
||||||
browserGatewayTarget.getBrowserGatewayTarget
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/org/:orgId/browser-gateway-target/:browserGatewayTargetId",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyLimits,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateBrowserGatewayTarget),
|
|
||||||
logActionAudit(ActionsEnum.updateBrowserGatewayTarget),
|
|
||||||
browserGatewayTarget.updateBrowserGatewayTarget
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/org/:orgId/browser-gateway-target/:browserGatewayTargetId",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteBrowserGatewayTarget),
|
|
||||||
logActionAudit(ActionsEnum.deleteBrowserGatewayTarget),
|
|
||||||
browserGatewayTarget.deleteBrowserGatewayTarget
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import * as orgIdp from "#private/routers/orgIdp";
|
|||||||
import * as billing from "#private/routers/billing";
|
import * as billing from "#private/routers/billing";
|
||||||
import * as license from "#private/routers/license";
|
import * as license from "#private/routers/license";
|
||||||
import * as resource from "#private/routers/resource";
|
import * as resource from "#private/routers/resource";
|
||||||
import * as browserTarget from "#private/routers/browserGatewayTarget";
|
|
||||||
import * as ssh from "#private/routers/ssh";
|
import * as ssh from "#private/routers/ssh";
|
||||||
import * as ws from "@server/routers/ws";
|
import * as ws from "@server/routers/ws";
|
||||||
|
import * as browserTarget from "#private/routers/browserGatewayTarget";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifySessionUserMiddleware,
|
verifySessionUserMiddleware,
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ import {
|
|||||||
userOrgs,
|
userOrgs,
|
||||||
sites,
|
sites,
|
||||||
Resource,
|
Resource,
|
||||||
SiteResource,
|
SiteResource
|
||||||
browserGatewayTarget
|
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { logAccessAudit } from "#private/lib/logAccessAudit";
|
import { logAccessAudit } from "#private/lib/logAccessAudit";
|
||||||
import { isLicensedOrSubscribed } from "#private/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#private/lib/isLicencedOrSubscribed";
|
||||||
@@ -291,16 +290,15 @@ export async function signSshKey(
|
|||||||
const publicResource = resource as Resource;
|
const publicResource = resource as Resource;
|
||||||
const targetRows = await db
|
const targetRows = await db
|
||||||
.select({
|
.select({
|
||||||
siteId: browserGatewayTarget.siteId,
|
siteId: targets.siteId,
|
||||||
ip: browserGatewayTarget.destination
|
ip: targets.ip
|
||||||
})
|
})
|
||||||
.from(browserGatewayTarget)
|
.from(targets)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(
|
eq(targets.resourceId, publicResource.resourceId),
|
||||||
browserGatewayTarget.resourceId,
|
eq(targets.enabled, true),
|
||||||
publicResource.resourceId
|
eq(targets.mode, "ssh")
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
browserGatewayTarget,
|
|
||||||
BrowserGatewayTarget,
|
|
||||||
clients,
|
clients,
|
||||||
clientSiteResourcesAssociationsCache,
|
clientSiteResourcesAssociationsCache,
|
||||||
clientSitesAssociationsCache,
|
clientSitesAssociationsCache,
|
||||||
@@ -16,7 +14,7 @@ import {
|
|||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { initPeerAddHandshake, updatePeer } from "../olm/peers";
|
import { initPeerAddHandshake, updatePeer } from "../olm/peers";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { decrypt } from "@server/lib/crypto";
|
import { decrypt } from "@server/lib/crypto";
|
||||||
import {
|
import {
|
||||||
@@ -211,7 +209,13 @@ export async function buildTargetConfigurationForNewtClient(
|
|||||||
})
|
})
|
||||||
.from(targets)
|
.from(targets)
|
||||||
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||||
.where(and(eq(targets.siteId, siteId), eq(targets.enabled, true)));
|
.where(
|
||||||
|
and(
|
||||||
|
eq(targets.siteId, siteId),
|
||||||
|
eq(targets.enabled, true),
|
||||||
|
inArray(targets.mode, ["http", "udp", "tcp"])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const allHealthChecks = await db
|
const allHealthChecks = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -236,10 +240,27 @@ export async function buildTargetConfigurationForNewtClient(
|
|||||||
.from(targetHealthCheck)
|
.from(targetHealthCheck)
|
||||||
.where(eq(targetHealthCheck.siteId, siteId));
|
.where(eq(targetHealthCheck.siteId, siteId));
|
||||||
|
|
||||||
|
// Get all enabled targets with their resource mode information
|
||||||
const allBrowserGatewayTargets = await db
|
const allBrowserGatewayTargets = await db
|
||||||
.select()
|
.select({
|
||||||
.from(browserGatewayTarget)
|
resourceId: targets.resourceId,
|
||||||
.where(eq(browserGatewayTarget.siteId, siteId));
|
targetId: targets.targetId,
|
||||||
|
ip: targets.ip,
|
||||||
|
method: targets.method,
|
||||||
|
port: targets.port,
|
||||||
|
enabled: targets.enabled,
|
||||||
|
mode: resources.mode,
|
||||||
|
authToken: targets.authToken
|
||||||
|
})
|
||||||
|
.from(targets)
|
||||||
|
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(targets.siteId, siteId),
|
||||||
|
eq(targets.enabled, true),
|
||||||
|
inArray(targets.mode, ["ssh", "rdp", "vnc"])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const { tcpTargets, udpTargets } = allTargets.reduce(
|
const { tcpTargets, udpTargets } = allTargets.reduce(
|
||||||
(acc, target) => {
|
(acc, target) => {
|
||||||
@@ -315,12 +336,15 @@ export async function buildTargetConfigurationForNewtClient(
|
|||||||
|
|
||||||
const serverSecret = config.getRawConfig().server.secret!;
|
const serverSecret = config.getRawConfig().server.secret!;
|
||||||
const browserGatewayTargets = allBrowserGatewayTargets.map((t) => {
|
const browserGatewayTargets = allBrowserGatewayTargets.map((t) => {
|
||||||
|
if (!t.ip || !t.port || !t.authToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const decryptAuthToken = decrypt(t.authToken, serverSecret);
|
const decryptAuthToken = decrypt(t.authToken, serverSecret);
|
||||||
return {
|
return {
|
||||||
id: t.browserGatewayTargetId,
|
id: t.targetId,
|
||||||
type: t.type,
|
type: t.mode,
|
||||||
destination: t.destination,
|
destination: t.ip,
|
||||||
destinationPort: t.destinationPort,
|
destinationPort: t.port,
|
||||||
authToken: decryptAuthToken
|
authToken: decryptAuthToken
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BrowserGatewayTarget, Target, TargetHealthCheck } from "@server/db";
|
import { Target, TargetHealthCheck } from "@server/db";
|
||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||||
@@ -244,23 +244,27 @@ export async function removeTargets(
|
|||||||
|
|
||||||
export async function sendBrowserGatewayTargets(
|
export async function sendBrowserGatewayTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: BrowserGatewayTarget[],
|
targets: Target[],
|
||||||
version?: string | null
|
version?: string | null
|
||||||
) {
|
) {
|
||||||
if (targets.length === 0) return;
|
if (targets.length === 0) return;
|
||||||
|
|
||||||
const payload = targets.map((t) => {
|
// filter out the ones without auth tokens
|
||||||
|
const filteredTargets = targets.filter((t) => t.authToken);
|
||||||
|
if (filteredTargets.length === 0) return;
|
||||||
|
|
||||||
|
const payload = filteredTargets.map((t) => {
|
||||||
const decryptAuthToken = decrypt(
|
const decryptAuthToken = decrypt(
|
||||||
t.authToken,
|
t.authToken!,
|
||||||
config.getRawConfig().server.secret!
|
config.getRawConfig().server.secret!
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
id: t.browserGatewayTargetId,
|
id: t.targetId,
|
||||||
resourceId: t.resourceId,
|
resourceId: t.resourceId,
|
||||||
siteId: t.siteId,
|
siteId: t.siteId,
|
||||||
type: t.type,
|
type: t.mode,
|
||||||
destination: t.destination,
|
destination: t.ip,
|
||||||
destinationPort: t.destinationPort,
|
destinationPort: t.port,
|
||||||
authToken: decryptAuthToken
|
authToken: decryptAuthToken
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
alias,
|
alias,
|
||||||
browserGatewayTarget,
|
|
||||||
db,
|
db,
|
||||||
labels,
|
labels,
|
||||||
resourceHeaderAuth,
|
resourceHeaderAuth,
|
||||||
@@ -639,15 +638,8 @@ export async function listResources(
|
|||||||
.from(targets)
|
.from(targets)
|
||||||
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
||||||
.where(and(eq(sites.orgId, orgId), eq(sites.siteId, siteId)));
|
.where(and(eq(sites.orgId, orgId), eq(sites.siteId, siteId)));
|
||||||
const resourcesWithBrowserGateway = db
|
|
||||||
.select({ resourceId: browserGatewayTarget.resourceId })
|
|
||||||
.from(browserGatewayTarget)
|
|
||||||
.where(eq(browserGatewayTarget.siteId, siteId));
|
|
||||||
conditions.push(
|
conditions.push(
|
||||||
or(
|
or(inArray(resources.resourceId, resourcesWithSite))
|
||||||
inArray(resources.resourceId, resourcesWithSite),
|
|
||||||
inArray(resources.resourceId, resourcesWithBrowserGateway)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,30 +762,6 @@ export async function listResources(
|
|||||||
)
|
)
|
||||||
.leftJoin(sites, eq(targets.siteId, sites.siteId));
|
.leftJoin(sites, eq(targets.siteId, sites.siteId));
|
||||||
|
|
||||||
const allBgTargetSites =
|
|
||||||
resourceIdList.length === 0
|
|
||||||
? []
|
|
||||||
: await db
|
|
||||||
.select({
|
|
||||||
resourceId: browserGatewayTarget.resourceId,
|
|
||||||
siteId: browserGatewayTarget.siteId,
|
|
||||||
siteName: sites.name,
|
|
||||||
siteNiceId: sites.niceId,
|
|
||||||
siteOnline: sites.online,
|
|
||||||
siteType: sites.type
|
|
||||||
})
|
|
||||||
.from(browserGatewayTarget)
|
|
||||||
.where(
|
|
||||||
inArray(
|
|
||||||
browserGatewayTarget.resourceId,
|
|
||||||
resourceIdList
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
sites,
|
|
||||||
eq(sites.siteId, browserGatewayTarget.siteId)
|
|
||||||
);
|
|
||||||
|
|
||||||
// avoids TS issues with reduce/never[]
|
// avoids TS issues with reduce/never[]
|
||||||
const map = new Map<number, ResourceWithTargets>();
|
const map = new Map<number, ResourceWithTargets>();
|
||||||
|
|
||||||
@@ -856,21 +824,6 @@ export async function listResources(
|
|||||||
online: isLocal ? undefined : Boolean(t.siteOnline)
|
online: isLocal ? undefined : Boolean(t.siteOnline)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const bgRaw = allBgTargetSites.filter(
|
|
||||||
(t) => t.resourceId === entry.resourceId
|
|
||||||
);
|
|
||||||
for (const t of bgRaw) {
|
|
||||||
if (typeof t.siteId !== "number" || siteById.has(t.siteId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const isLocal = t.siteType === "local";
|
|
||||||
siteById.set(t.siteId, {
|
|
||||||
siteId: t.siteId,
|
|
||||||
siteName: t.siteName ?? "",
|
|
||||||
siteNiceId: t.siteNiceId ?? "",
|
|
||||||
online: isLocal ? undefined : Boolean(t.siteOnline)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
entry.sites = Array.from(siteById.values());
|
entry.sites = Array.from(siteById.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
userSites,
|
userSites,
|
||||||
labels,
|
labels,
|
||||||
siteLabels,
|
siteLabels,
|
||||||
browserGatewayTarget,
|
|
||||||
type Label
|
type Label
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import cache from "#dynamic/lib/cache";
|
import cache from "#dynamic/lib/cache";
|
||||||
@@ -241,10 +240,6 @@ function querySitesBase() {
|
|||||||
ON ${siteResources.networkId} = ${siteNetworks.networkId}
|
ON ${siteResources.networkId} = ${siteNetworks.networkId}
|
||||||
WHERE ${siteNetworks.siteId} = ${sites.siteId}
|
WHERE ${siteNetworks.siteId} = ${sites.siteId}
|
||||||
AND ${siteResources.orgId} = ${sites.orgId}
|
AND ${siteResources.orgId} = ${sites.orgId}
|
||||||
) + (
|
|
||||||
SELECT COUNT(DISTINCT ${browserGatewayTarget.resourceId})
|
|
||||||
FROM ${browserGatewayTarget}
|
|
||||||
WHERE ${browserGatewayTarget.siteId} = ${sites.siteId}
|
|
||||||
)`,
|
)`,
|
||||||
status: sites.status
|
status: sites.status
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ import {
|
|||||||
fireHealthCheckUnhealthyAlert,
|
fireHealthCheckUnhealthyAlert,
|
||||||
fireHealthCheckUnknownAlert
|
fireHealthCheckUnknownAlert
|
||||||
} from "@server/lib/alerts";
|
} from "@server/lib/alerts";
|
||||||
|
import { encrypt } from "@server/lib/crypto";
|
||||||
|
import { generateId } from "@server/auth/sessions/app";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
const createTargetParamsSchema = z.strictObject({
|
const createTargetParamsSchema = z.strictObject({
|
||||||
resourceId: z.coerce.number().int().positive()
|
resourceId: z.coerce.number().int().positive()
|
||||||
@@ -32,6 +35,7 @@ const createTargetParamsSchema = z.strictObject({
|
|||||||
const createTargetSchema = z.strictObject({
|
const createTargetSchema = z.strictObject({
|
||||||
siteId: z.int().positive(),
|
siteId: z.int().positive(),
|
||||||
ip: z.string().refine(isTargetValid),
|
ip: z.string().refine(isTargetValid),
|
||||||
|
mode: z.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"]).optional(),
|
||||||
method: z.string().optional().nullable(),
|
method: z.string().optional().nullable(),
|
||||||
port: z.int().min(1).max(65535),
|
port: z.int().min(1).max(65535),
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
@@ -161,6 +165,12 @@ export async function createTarget(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const plainToken = generateId(48);
|
||||||
|
const encryptedToken = encrypt(
|
||||||
|
plainToken,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
|
||||||
let newTarget: Target[] = [];
|
let newTarget: Target[] = [];
|
||||||
let targetIps: string[] = [];
|
let targetIps: string[] = [];
|
||||||
let healthCheck: TargetHealthCheck[] = [];
|
let healthCheck: TargetHealthCheck[] = [];
|
||||||
@@ -191,6 +201,9 @@ export async function createTarget(
|
|||||||
.values({
|
.values({
|
||||||
resourceId,
|
resourceId,
|
||||||
...targetData,
|
...targetData,
|
||||||
|
mode: (targetData.mode ??
|
||||||
|
resource.mode ??
|
||||||
|
"http") as Target["mode"],
|
||||||
priority: targetData.priority || 100
|
priority: targetData.priority || 100
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
@@ -226,6 +239,10 @@ export async function createTarget(
|
|||||||
resourceId,
|
resourceId,
|
||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
ip: targetData.ip,
|
ip: targetData.ip,
|
||||||
|
mode: (targetData.mode ??
|
||||||
|
resource.mode ??
|
||||||
|
"http") as Target["mode"],
|
||||||
|
authToken: encryptedToken,
|
||||||
method: targetData.method,
|
method: targetData.method,
|
||||||
port: targetData.port,
|
port: targetData.port,
|
||||||
internalPort,
|
internalPort,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ function queryTargets(resourceId: number) {
|
|||||||
.select({
|
.select({
|
||||||
targetId: targets.targetId,
|
targetId: targets.targetId,
|
||||||
ip: targets.ip,
|
ip: targets.ip,
|
||||||
|
mode: targets.mode,
|
||||||
method: targets.method,
|
method: targets.method,
|
||||||
port: targets.port,
|
port: targets.port,
|
||||||
enabled: targets.enabled,
|
enabled: targets.enabled,
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ const updateTargetBodySchema = z
|
|||||||
.strictObject({
|
.strictObject({
|
||||||
siteId: z.int().positive(),
|
siteId: z.int().positive(),
|
||||||
ip: z.string().refine(isTargetValid),
|
ip: z.string().refine(isTargetValid),
|
||||||
|
mode: z
|
||||||
|
.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"])
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
method: z.string().min(1).max(10).optional().nullable(),
|
method: z.string().min(1).max(10).optional().nullable(),
|
||||||
port: z.int().min(1).max(65535).optional(),
|
port: z.int().min(1).max(65535).optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
@@ -184,6 +188,8 @@ export async function updateTarget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pathMatchTypeRemoved = parsedBody.data.pathMatchType === null;
|
const pathMatchTypeRemoved = parsedBody.data.pathMatchType === null;
|
||||||
|
const nextMode =
|
||||||
|
parsedBody.data.mode === null ? undefined : parsedBody.data.mode;
|
||||||
|
|
||||||
let updatedTarget: any;
|
let updatedTarget: any;
|
||||||
let updatedHc: any;
|
let updatedHc: any;
|
||||||
@@ -193,6 +199,7 @@ export async function updateTarget(
|
|||||||
.set({
|
.set({
|
||||||
siteId: parsedBody.data.siteId,
|
siteId: parsedBody.data.siteId,
|
||||||
ip: parsedBody.data.ip,
|
ip: parsedBody.data.ip,
|
||||||
|
mode: nextMode,
|
||||||
method: parsedBody.data.method,
|
method: parsedBody.data.method,
|
||||||
port: parsedBody.data.port,
|
port: parsedBody.data.port,
|
||||||
internalPort,
|
internalPort,
|
||||||
|
|||||||
@@ -138,11 +138,6 @@ export function ProxyResourceTargetsForm({
|
|||||||
const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] =
|
const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] =
|
||||||
useState<LocalTarget | null>(null);
|
useState<LocalTarget | null>(null);
|
||||||
|
|
||||||
const [bgDestination, setBgDestination] = useState("");
|
|
||||||
const [bgDestinationPort, setBgDestinationPort] = useState("");
|
|
||||||
const [bgSiteId, setBgSiteId] = useState<number | null>(null);
|
|
||||||
const [bgTargetId, setBgTargetId] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const initializeDockerForSite = async (siteId: number) => {
|
const initializeDockerForSite = async (siteId: number) => {
|
||||||
if (dockerStates.has(siteId)) {
|
if (dockerStates.has(siteId)) {
|
||||||
return;
|
return;
|
||||||
@@ -207,42 +202,6 @@ export function ProxyResourceTargetsForm({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Browser-gateway targets (edit mode only)
|
|
||||||
const { data: bgTargetsResponse } = useQuery({
|
|
||||||
queryKey: ["browserGatewayTargets", resource?.resourceId, orgId],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await api.get(
|
|
||||||
`/org/${orgId}/resource/${resource!.resourceId}/browser-gateway-targets`
|
|
||||||
);
|
|
||||||
return res.data.data as {
|
|
||||||
targets: Array<{
|
|
||||||
browserGatewayTargetId: number;
|
|
||||||
resourceId: number;
|
|
||||||
siteId: number;
|
|
||||||
type: string;
|
|
||||||
destination: string;
|
|
||||||
destinationPort: number;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
enabled: !!resource
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!bgTargetsResponse?.targets?.length) return;
|
|
||||||
const bgt = bgTargetsResponse.targets[0];
|
|
||||||
setBgDestination(bgt.destination);
|
|
||||||
setBgDestinationPort(String(bgt.destinationPort));
|
|
||||||
setBgSiteId(bgt.siteId);
|
|
||||||
setBgTargetId(bgt.browserGatewayTargetId);
|
|
||||||
}, [bgTargetsResponse]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (sites.length > 0 && bgSiteId === null) {
|
|
||||||
setBgSiteId(sites[0].siteId);
|
|
||||||
}
|
|
||||||
}, [sites, bgSiteId]);
|
|
||||||
|
|
||||||
const updateTarget = useCallback(
|
const updateTarget = useCallback(
|
||||||
(targetId: number, data: Partial<LocalTarget>) => {
|
(targetId: number, data: Partial<LocalTarget>) => {
|
||||||
setTargets((prevTargets) => {
|
setTargets((prevTargets) => {
|
||||||
@@ -603,6 +562,8 @@ export function ProxyResourceTargetsForm({
|
|||||||
const newTarget: LocalTarget = {
|
const newTarget: LocalTarget = {
|
||||||
targetId: -Date.now(),
|
targetId: -Date.now(),
|
||||||
ip: "",
|
ip: "",
|
||||||
|
mode: ((resource?.mode as LocalTarget["mode"]) ??
|
||||||
|
(isHttp ? "http" : "tcp")) as LocalTarget["mode"],
|
||||||
method: isHttp ? "http" : null,
|
method: isHttp ? "http" : null,
|
||||||
port: 0,
|
port: 0,
|
||||||
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
||||||
|
|||||||
@@ -31,21 +31,10 @@ import { GetResourceResponse } from "@server/routers/resource";
|
|||||||
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
||||||
|
|
||||||
type ExistingTarget = {
|
type ExistingTarget = {
|
||||||
browserGatewayTargetId: number;
|
targetId: number;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sshFormSchema = z.object({
|
|
||||||
authDaemonPort: z.string().refine(
|
|
||||||
(val) => {
|
|
||||||
if (!val) return true;
|
|
||||||
const n = Number(val);
|
|
||||||
return Number.isInteger(n) && n >= 1 && n <= 65535;
|
|
||||||
},
|
|
||||||
{ message: "Port must be between 1 and 65535" }
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function SshSettingsPage(props: {
|
export default function SshSettingsPage(props: {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
}) {
|
}) {
|
||||||
@@ -61,7 +50,7 @@ export default function SshSettingsPage(props: {
|
|||||||
<PaidFeaturesAlert
|
<PaidFeaturesAlert
|
||||||
tiers={tierMatrix[TierFeature.AdvancedPublicResources]}
|
tiers={tierMatrix[TierFeature.AdvancedPublicResources]}
|
||||||
/>
|
/>
|
||||||
<SshServerForm
|
<RdpServerForm
|
||||||
orgId={params.orgId}
|
orgId={params.orgId}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
updateResource={updateResource}
|
updateResource={updateResource}
|
||||||
@@ -71,7 +60,7 @@ export default function SshSettingsPage(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SshServerForm({
|
function RdpServerForm({
|
||||||
orgId,
|
orgId,
|
||||||
resource,
|
resource,
|
||||||
updateResource,
|
updateResource,
|
||||||
@@ -101,20 +90,18 @@ function SshServerForm({
|
|||||||
useState<ExistingTarget | null>(null);
|
useState<ExistingTarget | null>(null);
|
||||||
|
|
||||||
const { data: bgTargetsResponse } = useQuery({
|
const { data: bgTargetsResponse } = useQuery({
|
||||||
queryKey: ["browserGatewayTargets", resource.resourceId, orgId],
|
queryKey: ["resourceTargets", resource.resourceId, orgId, "rdp"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await api.get(
|
const res = await api.get(`/resource/${resource.resourceId}/targets`);
|
||||||
`/org/${orgId}/resource/${resource.resourceId}/browser-gateway-targets`
|
|
||||||
);
|
|
||||||
return res.data.data as {
|
return res.data.data as {
|
||||||
targets: Array<{
|
targets: Array<{
|
||||||
browserGatewayTargetId: number;
|
targetId: number;
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
siteName?: string;
|
siteName?: string;
|
||||||
type: string;
|
mode: string | null;
|
||||||
destination: string;
|
ip: string;
|
||||||
destinationPort: number;
|
port: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -122,14 +109,17 @@ function SshServerForm({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bgTargetsResponse?.targets?.length) return;
|
if (!bgTargetsResponse?.targets?.length) return;
|
||||||
const targets = bgTargetsResponse.targets;
|
const targets = bgTargetsResponse.targets.filter(
|
||||||
|
(t) => t.mode === "rdp"
|
||||||
|
);
|
||||||
|
if (!targets.length) return;
|
||||||
const first = targets[0];
|
const first = targets[0];
|
||||||
|
|
||||||
setBgDestination(first.destination);
|
setBgDestination(first.ip);
|
||||||
setBgDestinationPort(String(first.destinationPort));
|
setBgDestinationPort(String(first.port));
|
||||||
setExistingTargets(
|
setExistingTargets(
|
||||||
targets.map((t) => ({
|
targets.map((t) => ({
|
||||||
browserGatewayTargetId: t.browserGatewayTargetId,
|
targetId: t.targetId,
|
||||||
siteId: t.siteId
|
siteId: t.siteId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -159,9 +149,7 @@ function SshServerForm({
|
|||||||
);
|
);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
toDelete.map((t) =>
|
toDelete.map((t) =>
|
||||||
api.delete(
|
api.delete(`/target/${t.targetId}`)
|
||||||
`/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -171,12 +159,13 @@ function SshServerForm({
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
toUpdate.map((t) =>
|
toUpdate.map((t) =>
|
||||||
api.post(
|
api.post(
|
||||||
`/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}`,
|
`/target/${t.targetId}`,
|
||||||
{
|
{
|
||||||
type: "rdp",
|
mode: "rdp",
|
||||||
destination: bgDestination,
|
ip: bgDestination,
|
||||||
destinationPort: Number(bgDestinationPort),
|
port: Number(bgDestinationPort),
|
||||||
siteId: t.siteId
|
siteId: t.siteId,
|
||||||
|
hcEnabled: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -188,20 +177,20 @@ function SshServerForm({
|
|||||||
const created = await Promise.all(
|
const created = await Promise.all(
|
||||||
toCreate.map((s) =>
|
toCreate.map((s) =>
|
||||||
api.put(
|
api.put(
|
||||||
`/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`,
|
`/resource/${resource.resourceId}/target`,
|
||||||
{
|
{
|
||||||
siteId: s.siteId,
|
siteId: s.siteId,
|
||||||
type: "rdp",
|
mode: "rdp",
|
||||||
destination: bgDestination,
|
ip: bgDestination,
|
||||||
destinationPort: Number(bgDestinationPort)
|
port: Number(bgDestinationPort),
|
||||||
|
hcEnabled: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const newTargets: ExistingTarget[] = created.map((res, i) => ({
|
const newTargets: ExistingTarget[] = created.map((res, i) => ({
|
||||||
browserGatewayTargetId:
|
targetId: res.data.data.targetId,
|
||||||
res.data.data.browserGatewayTargetId,
|
|
||||||
siteId: toCreate[i].siteId
|
siteId: toCreate[i].siteId
|
||||||
}));
|
}));
|
||||||
setExistingTargets([...toUpdate, ...newTargets]);
|
setExistingTargets([...toUpdate, ...newTargets]);
|
||||||
|
|||||||
@@ -54,8 +54,9 @@ import { GetResourceResponse } from "@server/routers/resource";
|
|||||||
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
||||||
|
|
||||||
type ExistingTarget = {
|
type ExistingTarget = {
|
||||||
browserGatewayTargetId: number;
|
targetId: number;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
|
authToken?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sshFormSchema = z.object({
|
const sshFormSchema = z.object({
|
||||||
@@ -154,20 +155,19 @@ function SshServerForm({
|
|||||||
const [nativeSiteOpen, setNativeSiteOpen] = useState(false);
|
const [nativeSiteOpen, setNativeSiteOpen] = useState(false);
|
||||||
|
|
||||||
const { data: bgTargetsResponse } = useQuery({
|
const { data: bgTargetsResponse } = useQuery({
|
||||||
queryKey: ["browserGatewayTargets", resource.resourceId, orgId],
|
queryKey: ["resourceTargets", resource.resourceId, orgId, "ssh"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await api.get(
|
const res = await api.get(`/resource/${resource.resourceId}/targets`);
|
||||||
`/org/${orgId}/resource/${resource.resourceId}/browser-gateway-targets`
|
|
||||||
);
|
|
||||||
return res.data.data as {
|
return res.data.data as {
|
||||||
targets: Array<{
|
targets: Array<{
|
||||||
browserGatewayTargetId: number;
|
targetId: number;
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
siteName?: string;
|
siteName?: string;
|
||||||
type: string;
|
mode: string | null;
|
||||||
destination: string;
|
ip: string;
|
||||||
destinationPort: number;
|
port: number;
|
||||||
|
authToken?: string | null;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,10 @@ function SshServerForm({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bgTargetsResponse?.targets?.length) return;
|
if (!bgTargetsResponse?.targets?.length) return;
|
||||||
const targets = bgTargetsResponse.targets;
|
const targets = bgTargetsResponse.targets.filter(
|
||||||
|
(t) => t.mode === "ssh"
|
||||||
|
);
|
||||||
|
if (!targets.length) return;
|
||||||
const first = targets[0];
|
const first = targets[0];
|
||||||
if (isNativeInitially) {
|
if (isNativeInitially) {
|
||||||
setSelectedNativeSite({
|
setSelectedNativeSite({
|
||||||
@@ -184,16 +187,18 @@ function SshServerForm({
|
|||||||
type: "newt" as const
|
type: "newt" as const
|
||||||
});
|
});
|
||||||
setNativeExistingTarget({
|
setNativeExistingTarget({
|
||||||
browserGatewayTargetId: first.browserGatewayTargetId,
|
targetId: first.targetId,
|
||||||
siteId: first.siteId
|
siteId: first.siteId,
|
||||||
|
authToken: first.authToken
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setBgDestination(first.destination);
|
setBgDestination(first.ip);
|
||||||
setBgDestinationPort(String(first.destinationPort));
|
setBgDestinationPort(String(first.port));
|
||||||
setExistingTargets(
|
setExistingTargets(
|
||||||
targets.map((t) => ({
|
targets.map((t) => ({
|
||||||
browserGatewayTargetId: t.browserGatewayTargetId,
|
targetId: t.targetId,
|
||||||
siteId: t.siteId
|
siteId: t.siteId,
|
||||||
|
authToken: t.authToken
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
setSelectedSites(
|
setSelectedSites(
|
||||||
@@ -236,28 +241,31 @@ function SshServerForm({
|
|||||||
if (selectedNativeSite) {
|
if (selectedNativeSite) {
|
||||||
if (nativeExistingTarget) {
|
if (nativeExistingTarget) {
|
||||||
await api.post(
|
await api.post(
|
||||||
`/org/${orgId}/browser-gateway-target/${nativeExistingTarget.browserGatewayTargetId}`,
|
`/target/${nativeExistingTarget.targetId}`,
|
||||||
{
|
{
|
||||||
type: "ssh",
|
mode: "ssh",
|
||||||
destination: "localhost",
|
ip: "localhost",
|
||||||
destinationPort: 22,
|
port: 22,
|
||||||
siteId: selectedNativeSite.siteId
|
siteId: selectedNativeSite.siteId,
|
||||||
|
authToken: nativeExistingTarget.authToken,
|
||||||
|
hcEnabled: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const res = await api.put(
|
const res = await api.put(
|
||||||
`/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`,
|
`/resource/${resource.resourceId}/target`,
|
||||||
{
|
{
|
||||||
siteId: selectedNativeSite.siteId,
|
siteId: selectedNativeSite.siteId,
|
||||||
type: "ssh",
|
mode: "ssh",
|
||||||
destination: "localhost",
|
ip: "localhost",
|
||||||
destinationPort: 22
|
port: 22,
|
||||||
|
hcEnabled: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
setNativeExistingTarget({
|
setNativeExistingTarget({
|
||||||
browserGatewayTargetId:
|
targetId: res.data.data.targetId,
|
||||||
res.data.data.browserGatewayTargetId,
|
siteId: selectedNativeSite.siteId,
|
||||||
siteId: selectedNativeSite.siteId
|
authToken: res.data.data.authToken
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,9 +283,7 @@ function SshServerForm({
|
|||||||
);
|
);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
toDelete.map((t) =>
|
toDelete.map((t) =>
|
||||||
api.delete(
|
api.delete(`/target/${t.targetId}`)
|
||||||
`/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -287,12 +293,14 @@ function SshServerForm({
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
toUpdate.map((t) =>
|
toUpdate.map((t) =>
|
||||||
api.post(
|
api.post(
|
||||||
`/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}`,
|
`/target/${t.targetId}`,
|
||||||
{
|
{
|
||||||
type: "ssh",
|
mode: "ssh",
|
||||||
destination: bgDestination,
|
ip: bgDestination,
|
||||||
destinationPort: Number(bgDestinationPort),
|
port: Number(bgDestinationPort),
|
||||||
siteId: t.siteId
|
siteId: t.siteId,
|
||||||
|
authToken: t.authToken,
|
||||||
|
hcEnabled: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -304,12 +312,13 @@ function SshServerForm({
|
|||||||
const created = await Promise.all(
|
const created = await Promise.all(
|
||||||
toCreate.map((s) =>
|
toCreate.map((s) =>
|
||||||
api.put(
|
api.put(
|
||||||
`/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`,
|
`/resource/${resource.resourceId}/target`,
|
||||||
{
|
{
|
||||||
siteId: s.siteId,
|
siteId: s.siteId,
|
||||||
type: "ssh",
|
mode: "ssh",
|
||||||
destination: bgDestination,
|
ip: bgDestination,
|
||||||
destinationPort: Number(bgDestinationPort)
|
port: Number(bgDestinationPort),
|
||||||
|
hcEnabled: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -317,9 +326,9 @@ function SshServerForm({
|
|||||||
|
|
||||||
const newTargets: ExistingTarget[] = created.map(
|
const newTargets: ExistingTarget[] = created.map(
|
||||||
(res, i) => ({
|
(res, i) => ({
|
||||||
browserGatewayTargetId:
|
targetId: res.data.data.targetId,
|
||||||
res.data.data.browserGatewayTargetId,
|
siteId: toCreate[i].siteId,
|
||||||
siteId: toCreate[i].siteId
|
authToken: res.data.data.authToken
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
setExistingTargets([...toUpdate, ...newTargets]);
|
setExistingTargets([...toUpdate, ...newTargets]);
|
||||||
|
|||||||
@@ -29,21 +29,10 @@ import { GetResourceResponse } from "@server/routers/resource";
|
|||||||
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
||||||
|
|
||||||
type ExistingTarget = {
|
type ExistingTarget = {
|
||||||
browserGatewayTargetId: number;
|
targetId: number;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sshFormSchema = z.object({
|
|
||||||
authDaemonPort: z.string().refine(
|
|
||||||
(val) => {
|
|
||||||
if (!val) return true;
|
|
||||||
const n = Number(val);
|
|
||||||
return Number.isInteger(n) && n >= 1 && n <= 65535;
|
|
||||||
},
|
|
||||||
{ message: "Port must be between 1 and 65535" }
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function SshSettingsPage(props: {
|
export default function SshSettingsPage(props: {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
}) {
|
}) {
|
||||||
@@ -59,7 +48,7 @@ export default function SshSettingsPage(props: {
|
|||||||
<PaidFeaturesAlert
|
<PaidFeaturesAlert
|
||||||
tiers={tierMatrix[TierFeature.AdvancedPublicResources]}
|
tiers={tierMatrix[TierFeature.AdvancedPublicResources]}
|
||||||
/>
|
/>
|
||||||
<SshServerForm
|
<VncServerForm
|
||||||
orgId={params.orgId}
|
orgId={params.orgId}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
updateResource={updateResource}
|
updateResource={updateResource}
|
||||||
@@ -69,7 +58,7 @@ export default function SshSettingsPage(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SshServerForm({
|
function VncServerForm({
|
||||||
orgId,
|
orgId,
|
||||||
resource,
|
resource,
|
||||||
updateResource,
|
updateResource,
|
||||||
@@ -99,20 +88,18 @@ function SshServerForm({
|
|||||||
useState<ExistingTarget | null>(null);
|
useState<ExistingTarget | null>(null);
|
||||||
|
|
||||||
const { data: bgTargetsResponse } = useQuery({
|
const { data: bgTargetsResponse } = useQuery({
|
||||||
queryKey: ["browserGatewayTargets", resource.resourceId, orgId],
|
queryKey: ["resourceTargets", resource.resourceId, orgId, "vnc"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await api.get(
|
const res = await api.get(`/resource/${resource.resourceId}/targets`);
|
||||||
`/org/${orgId}/resource/${resource.resourceId}/browser-gateway-targets`
|
|
||||||
);
|
|
||||||
return res.data.data as {
|
return res.data.data as {
|
||||||
targets: Array<{
|
targets: Array<{
|
||||||
browserGatewayTargetId: number;
|
targetId: number;
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
siteName?: string;
|
siteName?: string;
|
||||||
type: string;
|
mode: string | null;
|
||||||
destination: string;
|
ip: string;
|
||||||
destinationPort: number;
|
port: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -120,14 +107,17 @@ function SshServerForm({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bgTargetsResponse?.targets?.length) return;
|
if (!bgTargetsResponse?.targets?.length) return;
|
||||||
const targets = bgTargetsResponse.targets;
|
const targets = bgTargetsResponse.targets.filter(
|
||||||
|
(t) => t.mode === "vnc"
|
||||||
|
);
|
||||||
|
if (!targets.length) return;
|
||||||
const first = targets[0];
|
const first = targets[0];
|
||||||
|
|
||||||
setBgDestination(first.destination);
|
setBgDestination(first.ip);
|
||||||
setBgDestinationPort(String(first.destinationPort));
|
setBgDestinationPort(String(first.port));
|
||||||
setExistingTargets(
|
setExistingTargets(
|
||||||
targets.map((t) => ({
|
targets.map((t) => ({
|
||||||
browserGatewayTargetId: t.browserGatewayTargetId,
|
targetId: t.targetId,
|
||||||
siteId: t.siteId
|
siteId: t.siteId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -157,9 +147,7 @@ function SshServerForm({
|
|||||||
);
|
);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
toDelete.map((t) =>
|
toDelete.map((t) =>
|
||||||
api.delete(
|
api.delete(`/target/${t.targetId}`)
|
||||||
`/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -169,12 +157,13 @@ function SshServerForm({
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
toUpdate.map((t) =>
|
toUpdate.map((t) =>
|
||||||
api.post(
|
api.post(
|
||||||
`/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}`,
|
`/target/${t.targetId}`,
|
||||||
{
|
{
|
||||||
type: "vnc",
|
mode: "vnc",
|
||||||
destination: bgDestination,
|
ip: bgDestination,
|
||||||
destinationPort: Number(bgDestinationPort),
|
port: Number(bgDestinationPort),
|
||||||
siteId: t.siteId
|
siteId: t.siteId,
|
||||||
|
hcEnabled: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -186,20 +175,20 @@ function SshServerForm({
|
|||||||
const created = await Promise.all(
|
const created = await Promise.all(
|
||||||
toCreate.map((s) =>
|
toCreate.map((s) =>
|
||||||
api.put(
|
api.put(
|
||||||
`/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`,
|
`/resource/${resource.resourceId}/target`,
|
||||||
{
|
{
|
||||||
siteId: s.siteId,
|
siteId: s.siteId,
|
||||||
type: "vnc",
|
mode: "vnc",
|
||||||
destination: bgDestination,
|
ip: bgDestination,
|
||||||
destinationPort: Number(bgDestinationPort)
|
port: Number(bgDestinationPort),
|
||||||
|
hcEnabled: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const newTargets: ExistingTarget[] = created.map((res, i) => ({
|
const newTargets: ExistingTarget[] = created.map((res, i) => ({
|
||||||
browserGatewayTargetId:
|
targetId: res.data.data.targetId,
|
||||||
res.data.data.browserGatewayTargetId,
|
|
||||||
siteId: toCreate[i].siteId
|
siteId: toCreate[i].siteId
|
||||||
}));
|
}));
|
||||||
setExistingTargets([...toUpdate, ...newTargets]);
|
setExistingTargets([...toUpdate, ...newTargets]);
|
||||||
|
|||||||
@@ -498,12 +498,13 @@ export default function Page() {
|
|||||||
if (isNative) {
|
if (isNative) {
|
||||||
if (nativeSelectedSite) {
|
if (nativeSelectedSite) {
|
||||||
await api.put(
|
await api.put(
|
||||||
`/org/${orgId}/resource/${id}/browser-gateway-target`,
|
`/resource/${id}/target`,
|
||||||
{
|
{
|
||||||
siteId: nativeSelectedSite.siteId,
|
siteId: nativeSelectedSite.siteId,
|
||||||
type: "ssh",
|
mode: "ssh",
|
||||||
destination: "localhost",
|
ip: "localhost",
|
||||||
destinationPort: 22
|
port: 22,
|
||||||
|
hcEnabled: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -516,12 +517,13 @@ export default function Page() {
|
|||||||
: [];
|
: [];
|
||||||
for (const site of sitesToCreate) {
|
for (const site of sitesToCreate) {
|
||||||
await api.put(
|
await api.put(
|
||||||
`/org/${orgId}/resource/${id}/browser-gateway-target`,
|
`/resource/${id}/target`,
|
||||||
{
|
{
|
||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
type: "ssh",
|
mode: "ssh",
|
||||||
destination: bgDestination,
|
ip: bgDestination,
|
||||||
destinationPort: Number(bgDestinationPort)
|
port: Number(bgDestinationPort),
|
||||||
|
hcEnabled: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -533,12 +535,14 @@ export default function Page() {
|
|||||||
} else if (resourceType === "rdp" || resourceType === "vnc") {
|
} else if (resourceType === "rdp" || resourceType === "vnc") {
|
||||||
for (const site of bgSelectedSites) {
|
for (const site of bgSelectedSites) {
|
||||||
await api.put(
|
await api.put(
|
||||||
`/org/${orgId}/resource/${id}/browser-gateway-target`,
|
`/resource/${id}/target`,
|
||||||
{
|
{
|
||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
type: resourceType,
|
mode: resourceType,
|
||||||
destination: bgDestination,
|
ip: bgDestination,
|
||||||
destinationPort: Number(bgDestinationPort)
|
port: Number(bgDestinationPort),
|
||||||
|
authToken: null,
|
||||||
|
hcEnabled: false
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user