From 772ac8af73e4781ce89ea60b6187145304003e49 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 5 Jun 2026 15:30:42 -0700 Subject: [PATCH] Remove browser gateway targets for regular targets --- server/db/pg/schema/privateSchema.ts | 21 -- server/db/pg/schema/schema.ts | 7 +- server/db/sqlite/schema/privateSchema.ts | 23 --- server/db/sqlite/schema/schema.ts | 7 +- .../private/lib/traefik/getTraefikConfig.ts | 78 ++------ .../createBrowserGatewayTarget.ts | 187 ------------------ .../deleteBrowserGatewayTarget.ts | 130 ------------ .../getBrowserGatewayTarget.ts | 109 ---------- .../browserGatewayTarget/getBrowserTarget.ts | 53 ++--- .../routers/browserGatewayTarget/index.ts | 5 - .../listBrowserGatewayTargets.ts | 159 --------------- .../updateBrowserGatewayTarget.ts | 180 ----------------- server/private/routers/external.ts | 46 ----- server/private/routers/integration.ts | 41 ---- server/private/routers/internal.ts | 2 +- server/private/routers/ssh/signSshKey.ts | 16 +- server/routers/newt/buildConfiguration.ts | 46 +++-- server/routers/newt/targets.ts | 20 +- server/routers/resource/listResources.ts | 49 +---- server/routers/site/listSites.ts | 5 - server/routers/target/createTarget.ts | 17 ++ server/routers/target/listTargets.ts | 1 + server/routers/target/updateTarget.ts | 7 + .../public/ProxyResourceTargetsForm.tsx | 43 +--- .../resources/public/[niceId]/rdp/page.tsx | 69 +++---- .../resources/public/[niceId]/ssh/page.tsx | 95 +++++---- .../resources/public/[niceId]/vnc/page.tsx | 69 +++---- .../settings/resources/public/create/page.tsx | 28 +-- 28 files changed, 259 insertions(+), 1254 deletions(-) delete mode 100644 server/private/routers/browserGatewayTarget/createBrowserGatewayTarget.ts delete mode 100644 server/private/routers/browserGatewayTarget/deleteBrowserGatewayTarget.ts delete mode 100644 server/private/routers/browserGatewayTarget/getBrowserGatewayTarget.ts delete mode 100644 server/private/routers/browserGatewayTarget/listBrowserGatewayTargets.ts delete mode 100644 server/private/routers/browserGatewayTarget/updateBrowserGatewayTarget.ts diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index 5040808a9..229fc9ff0 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -580,24 +580,6 @@ export const trialNotifications = pgTable("trialNotifications", { 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; export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -645,6 +627,3 @@ export type AlertEmailRecipients = InferSelectModel< >; export type AlertWebhookActions = InferSelectModel; export type TrialNotification = InferSelectModel; -export type BrowserGatewayTarget = InferSelectModel< - typeof browserGatewayTarget ->; diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 6b4ce32b8..b7b34a5d7 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -290,7 +290,12 @@ export const targets = pgTable("targets", { pathMatchType: text("pathMatchType"), // exact, prefix, regex rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target 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", { diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index b235d26d5..ae7360780 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -588,26 +588,6 @@ export const trialNotifications = sqliteTable("trialNotifications", { 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; export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -647,6 +627,3 @@ export type AlertEmailAction = InferSelectModel; export type AlertEmailRecipient = InferSelectModel; export type AlertWebhookAction = InferSelectModel; export type TrialNotification = InferSelectModel; -export type BrowserGatewayTarget = InferSelectModel< - typeof browserGatewayTarget ->; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 492576cc6..639e3cf4f 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -322,7 +322,12 @@ export const targets = sqliteTable("targets", { pathMatchType: text("pathMatchType"), // exact, prefix, regex rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target 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", { diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 7ff452880..901c88f49 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -12,7 +12,6 @@ */ import { - browserGatewayTarget, certificates, db, domainNamespaces, @@ -182,6 +181,9 @@ export async function getTraefikConfig( const resourcesMap = new Map(); resourcesWithTargetsAndSites.forEach((row) => { + if (!["http", "tcp", "udp"].includes(row.mode)) { + return; + } const resourceId = row.resourceId; const resourceName = sanitize(row.resourceName) || ""; 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; maintenanceEstimatedTime: string | null; targets: { - browserGatewayTargetId: number; + targetId: number; bgType: string; siteId: number; siteType: string; siteOnline: boolean | null; subnet: string | null; - siteExitNodeId: number | null; }[]; }; const browserGatewayResourcesMap = new Map< @@ -310,66 +311,10 @@ export async function getTraefikConfig( >(); if (allowBrowserGatewayResources) { - // Query browser gateway targets for this exit node - const browserGatewayRows = await db - .select({ - // 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) { + for (const row of resourcesWithTargetsAndSites) { + if (!["ssh", "vnc", "rdp"].includes(row.mode)) { + return; + } if (filterOutNamespaceDomains && row.domainNamespaceId) { continue; } @@ -394,13 +339,12 @@ export async function getTraefikConfig( }); } browserGatewayResourcesMap.get(row.resourceId)!.targets.push({ - browserGatewayTargetId: row.browserGatewayTargetId, - bgType: row.bgType, + targetId: row.targetId, + bgType: row.mode, siteId: row.siteId, siteType: row.siteType, siteOnline: row.siteOnline, - subnet: row.subnet, - siteExitNodeId: row.siteExitNodeId + subnet: row.subnet }); } } diff --git a/server/private/routers/browserGatewayTarget/createBrowserGatewayTarget.ts b/server/private/routers/browserGatewayTarget/createBrowserGatewayTarget.ts deleted file mode 100644 index b26a1a8b6..000000000 --- a/server/private/routers/browserGatewayTarget/createBrowserGatewayTarget.ts +++ /dev/null @@ -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 { - 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(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" - ) - ); - } -} diff --git a/server/private/routers/browserGatewayTarget/deleteBrowserGatewayTarget.ts b/server/private/routers/browserGatewayTarget/deleteBrowserGatewayTarget.ts deleted file mode 100644 index 850944b29..000000000 --- a/server/private/routers/browserGatewayTarget/deleteBrowserGatewayTarget.ts +++ /dev/null @@ -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 { - 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" - ) - ); - } -} diff --git a/server/private/routers/browserGatewayTarget/getBrowserGatewayTarget.ts b/server/private/routers/browserGatewayTarget/getBrowserGatewayTarget.ts deleted file mode 100644 index 0ac7a8ce9..000000000 --- a/server/private/routers/browserGatewayTarget/getBrowserGatewayTarget.ts +++ /dev/null @@ -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 { - 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(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" - ) - ); - } -} diff --git a/server/private/routers/browserGatewayTarget/getBrowserTarget.ts b/server/private/routers/browserGatewayTarget/getBrowserTarget.ts index 51e16de75..b8e32d836 100644 --- a/server/private/routers/browserGatewayTarget/getBrowserTarget.ts +++ b/server/private/routers/browserGatewayTarget/getBrowserTarget.ts @@ -13,9 +13,8 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { browserGatewayTarget, db } from "@server/db"; -import { resources, targets } from "@server/db"; -import { eq } from "drizzle-orm"; +import { db, resources, targets } from "@server/db"; +import { eq, and, inArray } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -51,11 +50,11 @@ export async function getBrowserTarget( logger.info(`Retrieving browser target for domain: ${fullDomain}`); - const [browserTarget] = await db + const [row] = await db .select({ - destination: browserGatewayTarget.destination, - destinationPort: browserGatewayTarget.destinationPort, - authToken: browserGatewayTarget.authToken, + ip: targets.ip, + port: targets.port, + authToken: targets.authToken, resourceId: resources.resourceId, niceId: resources.niceId, name: resources.name, @@ -63,20 +62,18 @@ export async function getBrowserTarget( pamMode: resources.pamMode, authDaemonMode: resources.authDaemonMode }) - .from(browserGatewayTarget) - .innerJoin( - resources, - eq(browserGatewayTarget.resourceId, resources.resourceId) + .from(targets) + .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) + .where( + and( + eq(resources.fullDomain, fullDomain), + eq(targets.enabled, true), + inArray(targets.mode, ["ssh", "rdp", "vnc"]) + ) ) - .where(eq(resources.fullDomain, fullDomain)) .limit(1); - const decryptedAuthToken = decrypt( - browserTarget.authToken, - config.getRawConfig().server.secret! - ); - - if (!browserTarget) { + if (!row) { return next( createHttpError( HttpCode.NOT_FOUND, @@ -85,17 +82,21 @@ export async function getBrowserTarget( ); } + const decryptedAuthToken = row.authToken + ? decrypt(row.authToken, config.getRawConfig().server.secret!) + : ""; + return response(res, { data: { - ip: browserTarget.destination, - port: browserTarget.destinationPort, + ip: row.ip, + port: row.port, authToken: decryptedAuthToken, - pamMode: browserTarget.pamMode, - authDaemonMode: browserTarget.authDaemonMode, - orgId: browserTarget.orgId, - resourceId: browserTarget.resourceId, - niceId: browserTarget.niceId, - name: browserTarget.name + pamMode: row.pamMode, + authDaemonMode: row.authDaemonMode, + orgId: row.orgId, + resourceId: row.resourceId, + niceId: row.niceId, + name: row.name ?? "" }, success: true, error: false, diff --git a/server/private/routers/browserGatewayTarget/index.ts b/server/private/routers/browserGatewayTarget/index.ts index c9cd15dff..3c1b3d6f9 100644 --- a/server/private/routers/browserGatewayTarget/index.ts +++ b/server/private/routers/browserGatewayTarget/index.ts @@ -11,9 +11,4 @@ * 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"; diff --git a/server/private/routers/browserGatewayTarget/listBrowserGatewayTargets.ts b/server/private/routers/browserGatewayTarget/listBrowserGatewayTargets.ts deleted file mode 100644 index 5b3d1e5d0..000000000 --- a/server/private/routers/browserGatewayTarget/listBrowserGatewayTargets.ts +++ /dev/null @@ -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 { - 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(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" - ) - ); - } -} diff --git a/server/private/routers/browserGatewayTarget/updateBrowserGatewayTarget.ts b/server/private/routers/browserGatewayTarget/updateBrowserGatewayTarget.ts deleted file mode 100644 index 825407dc3..000000000 --- a/server/private/routers/browserGatewayTarget/updateBrowserGatewayTarget.ts +++ /dev/null @@ -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 { - 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 = {}; - 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(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" - ) - ); - } -} diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 0598a1514..881ba2277 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -31,7 +31,6 @@ import * as siteProvisioning from "#private/routers/siteProvisioning"; import * as eventStreamingDestination from "#private/routers/eventStreamingDestination"; import * as alertRule from "#private/routers/alertRule"; import * as healthChecks from "#private/routers/healthChecks"; -import * as browserGatewayTarget from "#private/routers/browserGatewayTarget"; import * as labels from "#private/routers/labels"; import * as client from "@server/routers/client"; import * as resource from "#private/routers/resource"; @@ -879,48 +878,3 @@ authenticated.post( verifyClientAccess, 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 -); diff --git a/server/private/routers/integration.ts b/server/private/routers/integration.ts index 542c806f4..820a843f0 100644 --- a/server/private/routers/integration.ts +++ b/server/private/routers/integration.ts @@ -16,7 +16,6 @@ import * as org from "#private/routers/org"; import * as logs from "#private/routers/auditLogs"; import * as alertEvents from "#private/routers/alertEvents"; import * as certificates from "#private/routers/certificates"; -import * as browserGatewayTarget from "#private/routers/browserGatewayTarget"; import { verifyApiKeyHasAction, @@ -216,43 +215,3 @@ authenticated.delete( logActionAudit(ActionsEnum.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 -); diff --git a/server/private/routers/internal.ts b/server/private/routers/internal.ts index f78acb48e..c45fe36b9 100644 --- a/server/private/routers/internal.ts +++ b/server/private/routers/internal.ts @@ -17,9 +17,9 @@ import * as orgIdp from "#private/routers/orgIdp"; import * as billing from "#private/routers/billing"; import * as license from "#private/routers/license"; import * as resource from "#private/routers/resource"; -import * as browserTarget from "#private/routers/browserGatewayTarget"; import * as ssh from "#private/routers/ssh"; import * as ws from "@server/routers/ws"; +import * as browserTarget from "#private/routers/browserGatewayTarget"; import { verifySessionUserMiddleware, diff --git a/server/private/routers/ssh/signSshKey.ts b/server/private/routers/ssh/signSshKey.ts index dac4ae62a..bcf8beab7 100644 --- a/server/private/routers/ssh/signSshKey.ts +++ b/server/private/routers/ssh/signSshKey.ts @@ -30,8 +30,7 @@ import { userOrgs, sites, Resource, - SiteResource, - browserGatewayTarget + SiteResource } from "@server/db"; import { logAccessAudit } from "#private/lib/logAccessAudit"; import { isLicensedOrSubscribed } from "#private/lib/isLicencedOrSubscribed"; @@ -291,16 +290,15 @@ export async function signSshKey( const publicResource = resource as Resource; const targetRows = await db .select({ - siteId: browserGatewayTarget.siteId, - ip: browserGatewayTarget.destination + siteId: targets.siteId, + ip: targets.ip }) - .from(browserGatewayTarget) + .from(targets) .where( and( - eq( - browserGatewayTarget.resourceId, - publicResource.resourceId - ) + eq(targets.resourceId, publicResource.resourceId), + eq(targets.enabled, true), + eq(targets.mode, "ssh") ) ); diff --git a/server/routers/newt/buildConfiguration.ts b/server/routers/newt/buildConfiguration.ts index 135920d6f..73bf2c630 100644 --- a/server/routers/newt/buildConfiguration.ts +++ b/server/routers/newt/buildConfiguration.ts @@ -1,6 +1,4 @@ import { - browserGatewayTarget, - BrowserGatewayTarget, clients, clientSiteResourcesAssociationsCache, clientSitesAssociationsCache, @@ -16,7 +14,7 @@ import { } from "@server/db"; import logger from "@server/logger"; 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 { decrypt } from "@server/lib/crypto"; import { @@ -211,7 +209,13 @@ export async function buildTargetConfigurationForNewtClient( }) .from(targets) .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 .select({ @@ -236,10 +240,27 @@ export async function buildTargetConfigurationForNewtClient( .from(targetHealthCheck) .where(eq(targetHealthCheck.siteId, siteId)); + // Get all enabled targets with their resource mode information const allBrowserGatewayTargets = await db - .select() - .from(browserGatewayTarget) - .where(eq(browserGatewayTarget.siteId, siteId)); + .select({ + resourceId: targets.resourceId, + 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( (acc, target) => { @@ -315,12 +336,15 @@ export async function buildTargetConfigurationForNewtClient( const serverSecret = config.getRawConfig().server.secret!; const browserGatewayTargets = allBrowserGatewayTargets.map((t) => { + if (!t.ip || !t.port || !t.authToken) { + return null; + } const decryptAuthToken = decrypt(t.authToken, serverSecret); return { - id: t.browserGatewayTargetId, - type: t.type, - destination: t.destination, - destinationPort: t.destinationPort, + id: t.targetId, + type: t.mode, + destination: t.ip, + destinationPort: t.port, authToken: decryptAuthToken }; }); diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index 6d8212b12..44aa34637 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -1,4 +1,4 @@ -import { BrowserGatewayTarget, Target, TargetHealthCheck } from "@server/db"; +import { Target, TargetHealthCheck } from "@server/db"; import { sendToClient } from "#dynamic/routers/ws"; import logger from "@server/logger"; import { canCompress } from "@server/lib/clientVersionChecks"; @@ -244,23 +244,27 @@ export async function removeTargets( export async function sendBrowserGatewayTargets( newtId: string, - targets: BrowserGatewayTarget[], + targets: Target[], version?: string | null ) { 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( - t.authToken, + t.authToken!, config.getRawConfig().server.secret! ); return { - id: t.browserGatewayTargetId, + id: t.targetId, resourceId: t.resourceId, siteId: t.siteId, - type: t.type, - destination: t.destination, - destinationPort: t.destinationPort, + type: t.mode, + destination: t.ip, + destinationPort: t.port, authToken: decryptAuthToken }; }); diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index c982cab9f..8e0a03384 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -1,6 +1,5 @@ import { alias, - browserGatewayTarget, db, labels, resourceHeaderAuth, @@ -639,15 +638,8 @@ export async function listResources( .from(targets) .innerJoin(sites, eq(targets.siteId, sites.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( - or( - inArray(resources.resourceId, resourcesWithSite), - inArray(resources.resourceId, resourcesWithBrowserGateway) - ) + or(inArray(resources.resourceId, resourcesWithSite)) ); } @@ -770,30 +762,6 @@ export async function listResources( ) .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[] const map = new Map(); @@ -856,21 +824,6 @@ export async function listResources( 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()); } diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index c217da489..c6abace5f 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -12,7 +12,6 @@ import { userSites, labels, siteLabels, - browserGatewayTarget, type Label } from "@server/db"; import cache from "#dynamic/lib/cache"; @@ -241,10 +240,6 @@ function querySitesBase() { ON ${siteResources.networkId} = ${siteNetworks.networkId} WHERE ${siteNetworks.siteId} = ${sites.siteId} AND ${siteResources.orgId} = ${sites.orgId} - ) + ( - SELECT COUNT(DISTINCT ${browserGatewayTarget.resourceId}) - FROM ${browserGatewayTarget} - WHERE ${browserGatewayTarget.siteId} = ${sites.siteId} )`, status: sites.status }) diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 53488e2b7..bb880b045 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -24,6 +24,9 @@ import { fireHealthCheckUnhealthyAlert, fireHealthCheckUnknownAlert } 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({ resourceId: z.coerce.number().int().positive() @@ -32,6 +35,7 @@ const createTargetParamsSchema = z.strictObject({ const createTargetSchema = z.strictObject({ siteId: z.int().positive(), ip: z.string().refine(isTargetValid), + mode: z.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"]).optional(), method: z.string().optional().nullable(), port: z.int().min(1).max(65535), 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 targetIps: string[] = []; let healthCheck: TargetHealthCheck[] = []; @@ -191,6 +201,9 @@ export async function createTarget( .values({ resourceId, ...targetData, + mode: (targetData.mode ?? + resource.mode ?? + "http") as Target["mode"], priority: targetData.priority || 100 }) .returning(); @@ -226,6 +239,10 @@ export async function createTarget( resourceId, siteId: site.siteId, ip: targetData.ip, + mode: (targetData.mode ?? + resource.mode ?? + "http") as Target["mode"], + authToken: encryptedToken, method: targetData.method, port: targetData.port, internalPort, diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index 47e9cdea5..1b2eb0ed5 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -34,6 +34,7 @@ function queryTargets(resourceId: number) { .select({ targetId: targets.targetId, ip: targets.ip, + mode: targets.mode, method: targets.method, port: targets.port, enabled: targets.enabled, diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 4b667d086..a5bb5fef3 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -27,6 +27,10 @@ const updateTargetBodySchema = z .strictObject({ siteId: z.int().positive(), 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(), port: z.int().min(1).max(65535).optional(), enabled: z.boolean().optional(), @@ -184,6 +188,8 @@ export async function updateTarget( } const pathMatchTypeRemoved = parsedBody.data.pathMatchType === null; + const nextMode = + parsedBody.data.mode === null ? undefined : parsedBody.data.mode; let updatedTarget: any; let updatedHc: any; @@ -193,6 +199,7 @@ export async function updateTarget( .set({ siteId: parsedBody.data.siteId, ip: parsedBody.data.ip, + mode: nextMode, method: parsedBody.data.method, port: parsedBody.data.port, internalPort, diff --git a/src/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm.tsx b/src/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm.tsx index 7289c2767..881d46b7b 100644 --- a/src/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm.tsx +++ b/src/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm.tsx @@ -138,11 +138,6 @@ export function ProxyResourceTargetsForm({ const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = useState(null); - const [bgDestination, setBgDestination] = useState(""); - const [bgDestinationPort, setBgDestinationPort] = useState(""); - const [bgSiteId, setBgSiteId] = useState(null); - const [bgTargetId, setBgTargetId] = useState(null); - const initializeDockerForSite = async (siteId: number) => { if (dockerStates.has(siteId)) { 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( (targetId: number, data: Partial) => { setTargets((prevTargets) => { @@ -603,6 +562,8 @@ export function ProxyResourceTargetsForm({ const newTarget: LocalTarget = { targetId: -Date.now(), ip: "", + mode: ((resource?.mode as LocalTarget["mode"]) ?? + (isHttp ? "http" : "tcp")) as LocalTarget["mode"], method: isHttp ? "http" : null, port: 0, siteId: sites.length > 0 ? sites[0].siteId : 0, diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/rdp/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/rdp/page.tsx index ee564156a..d2ed89601 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/rdp/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/rdp/page.tsx @@ -31,21 +31,10 @@ import { GetResourceResponse } from "@server/routers/resource"; import type { ResourceContextType } from "@app/contexts/resourceContext"; type ExistingTarget = { - browserGatewayTargetId: number; + targetId: 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: { params: Promise<{ orgId: string }>; }) { @@ -61,7 +50,7 @@ export default function SshSettingsPage(props: { - (null); const { data: bgTargetsResponse } = useQuery({ - queryKey: ["browserGatewayTargets", resource.resourceId, orgId], + queryKey: ["resourceTargets", resource.resourceId, orgId, "rdp"], queryFn: async () => { - const res = await api.get( - `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-targets` - ); + const res = await api.get(`/resource/${resource.resourceId}/targets`); return res.data.data as { targets: Array<{ - browserGatewayTargetId: number; + targetId: number; resourceId: number; siteId: number; siteName?: string; - type: string; - destination: string; - destinationPort: number; + mode: string | null; + ip: string; + port: number; }>; }; } @@ -122,14 +109,17 @@ function SshServerForm({ useEffect(() => { 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]; - setBgDestination(first.destination); - setBgDestinationPort(String(first.destinationPort)); + setBgDestination(first.ip); + setBgDestinationPort(String(first.port)); setExistingTargets( targets.map((t) => ({ - browserGatewayTargetId: t.browserGatewayTargetId, + targetId: t.targetId, siteId: t.siteId })) ); @@ -159,9 +149,7 @@ function SshServerForm({ ); await Promise.all( toDelete.map((t) => - api.delete( - `/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}` - ) + api.delete(`/target/${t.targetId}`) ) ); @@ -171,12 +159,13 @@ function SshServerForm({ await Promise.all( toUpdate.map((t) => api.post( - `/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}`, + `/target/${t.targetId}`, { - type: "rdp", - destination: bgDestination, - destinationPort: Number(bgDestinationPort), - siteId: t.siteId + mode: "rdp", + ip: bgDestination, + port: Number(bgDestinationPort), + siteId: t.siteId, + hcEnabled: false } ) ) @@ -188,20 +177,20 @@ function SshServerForm({ const created = await Promise.all( toCreate.map((s) => api.put( - `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`, + `/resource/${resource.resourceId}/target`, { siteId: s.siteId, - type: "rdp", - destination: bgDestination, - destinationPort: Number(bgDestinationPort) + mode: "rdp", + ip: bgDestination, + port: Number(bgDestinationPort), + hcEnabled: false } ) ) ); const newTargets: ExistingTarget[] = created.map((res, i) => ({ - browserGatewayTargetId: - res.data.data.browserGatewayTargetId, + targetId: res.data.data.targetId, siteId: toCreate[i].siteId })); setExistingTargets([...toUpdate, ...newTargets]); diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx index c769d28e0..7a85be60c 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/ssh/page.tsx @@ -54,8 +54,9 @@ import { GetResourceResponse } from "@server/routers/resource"; import type { ResourceContextType } from "@app/contexts/resourceContext"; type ExistingTarget = { - browserGatewayTargetId: number; + targetId: number; siteId: number; + authToken?: string | null; }; const sshFormSchema = z.object({ @@ -154,20 +155,19 @@ function SshServerForm({ const [nativeSiteOpen, setNativeSiteOpen] = useState(false); const { data: bgTargetsResponse } = useQuery({ - queryKey: ["browserGatewayTargets", resource.resourceId, orgId], + queryKey: ["resourceTargets", resource.resourceId, orgId, "ssh"], queryFn: async () => { - const res = await api.get( - `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-targets` - ); + const res = await api.get(`/resource/${resource.resourceId}/targets`); return res.data.data as { targets: Array<{ - browserGatewayTargetId: number; + targetId: number; resourceId: number; siteId: number; siteName?: string; - type: string; - destination: string; - destinationPort: number; + mode: string | null; + ip: string; + port: number; + authToken?: string | null; }>; }; } @@ -175,7 +175,10 @@ function SshServerForm({ useEffect(() => { 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]; if (isNativeInitially) { setSelectedNativeSite({ @@ -184,16 +187,18 @@ function SshServerForm({ type: "newt" as const }); setNativeExistingTarget({ - browserGatewayTargetId: first.browserGatewayTargetId, - siteId: first.siteId + targetId: first.targetId, + siteId: first.siteId, + authToken: first.authToken }); } else { - setBgDestination(first.destination); - setBgDestinationPort(String(first.destinationPort)); + setBgDestination(first.ip); + setBgDestinationPort(String(first.port)); setExistingTargets( targets.map((t) => ({ - browserGatewayTargetId: t.browserGatewayTargetId, - siteId: t.siteId + targetId: t.targetId, + siteId: t.siteId, + authToken: t.authToken })) ); setSelectedSites( @@ -236,28 +241,31 @@ function SshServerForm({ if (selectedNativeSite) { if (nativeExistingTarget) { await api.post( - `/org/${orgId}/browser-gateway-target/${nativeExistingTarget.browserGatewayTargetId}`, + `/target/${nativeExistingTarget.targetId}`, { - type: "ssh", - destination: "localhost", - destinationPort: 22, - siteId: selectedNativeSite.siteId + mode: "ssh", + ip: "localhost", + port: 22, + siteId: selectedNativeSite.siteId, + authToken: nativeExistingTarget.authToken, + hcEnabled: false } ); } else { const res = await api.put( - `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`, + `/resource/${resource.resourceId}/target`, { siteId: selectedNativeSite.siteId, - type: "ssh", - destination: "localhost", - destinationPort: 22 + mode: "ssh", + ip: "localhost", + port: 22, + hcEnabled: false } ); setNativeExistingTarget({ - browserGatewayTargetId: - res.data.data.browserGatewayTargetId, - siteId: selectedNativeSite.siteId + targetId: res.data.data.targetId, + siteId: selectedNativeSite.siteId, + authToken: res.data.data.authToken }); } } @@ -275,9 +283,7 @@ function SshServerForm({ ); await Promise.all( toDelete.map((t) => - api.delete( - `/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}` - ) + api.delete(`/target/${t.targetId}`) ) ); @@ -287,12 +293,14 @@ function SshServerForm({ await Promise.all( toUpdate.map((t) => api.post( - `/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}`, + `/target/${t.targetId}`, { - type: "ssh", - destination: bgDestination, - destinationPort: Number(bgDestinationPort), - siteId: t.siteId + mode: "ssh", + ip: bgDestination, + port: Number(bgDestinationPort), + siteId: t.siteId, + authToken: t.authToken, + hcEnabled: false } ) ) @@ -304,12 +312,13 @@ function SshServerForm({ const created = await Promise.all( toCreate.map((s) => api.put( - `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`, + `/resource/${resource.resourceId}/target`, { siteId: s.siteId, - type: "ssh", - destination: bgDestination, - destinationPort: Number(bgDestinationPort) + mode: "ssh", + ip: bgDestination, + port: Number(bgDestinationPort), + hcEnabled: false } ) ) @@ -317,9 +326,9 @@ function SshServerForm({ const newTargets: ExistingTarget[] = created.map( (res, i) => ({ - browserGatewayTargetId: - res.data.data.browserGatewayTargetId, - siteId: toCreate[i].siteId + targetId: res.data.data.targetId, + siteId: toCreate[i].siteId, + authToken: res.data.data.authToken }) ); setExistingTargets([...toUpdate, ...newTargets]); diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/vnc/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/vnc/page.tsx index 51efd0311..ecac78b6f 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/vnc/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/vnc/page.tsx @@ -29,21 +29,10 @@ import { GetResourceResponse } from "@server/routers/resource"; import type { ResourceContextType } from "@app/contexts/resourceContext"; type ExistingTarget = { - browserGatewayTargetId: number; + targetId: 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: { params: Promise<{ orgId: string }>; }) { @@ -59,7 +48,7 @@ export default function SshSettingsPage(props: { - (null); const { data: bgTargetsResponse } = useQuery({ - queryKey: ["browserGatewayTargets", resource.resourceId, orgId], + queryKey: ["resourceTargets", resource.resourceId, orgId, "vnc"], queryFn: async () => { - const res = await api.get( - `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-targets` - ); + const res = await api.get(`/resource/${resource.resourceId}/targets`); return res.data.data as { targets: Array<{ - browserGatewayTargetId: number; + targetId: number; resourceId: number; siteId: number; siteName?: string; - type: string; - destination: string; - destinationPort: number; + mode: string | null; + ip: string; + port: number; }>; }; } @@ -120,14 +107,17 @@ function SshServerForm({ useEffect(() => { 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]; - setBgDestination(first.destination); - setBgDestinationPort(String(first.destinationPort)); + setBgDestination(first.ip); + setBgDestinationPort(String(first.port)); setExistingTargets( targets.map((t) => ({ - browserGatewayTargetId: t.browserGatewayTargetId, + targetId: t.targetId, siteId: t.siteId })) ); @@ -157,9 +147,7 @@ function SshServerForm({ ); await Promise.all( toDelete.map((t) => - api.delete( - `/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}` - ) + api.delete(`/target/${t.targetId}`) ) ); @@ -169,12 +157,13 @@ function SshServerForm({ await Promise.all( toUpdate.map((t) => api.post( - `/org/${orgId}/browser-gateway-target/${t.browserGatewayTargetId}`, + `/target/${t.targetId}`, { - type: "vnc", - destination: bgDestination, - destinationPort: Number(bgDestinationPort), - siteId: t.siteId + mode: "vnc", + ip: bgDestination, + port: Number(bgDestinationPort), + siteId: t.siteId, + hcEnabled: false } ) ) @@ -186,20 +175,20 @@ function SshServerForm({ const created = await Promise.all( toCreate.map((s) => api.put( - `/org/${orgId}/resource/${resource.resourceId}/browser-gateway-target`, + `/resource/${resource.resourceId}/target`, { siteId: s.siteId, - type: "vnc", - destination: bgDestination, - destinationPort: Number(bgDestinationPort) + mode: "vnc", + ip: bgDestination, + port: Number(bgDestinationPort), + hcEnabled: false } ) ) ); const newTargets: ExistingTarget[] = created.map((res, i) => ({ - browserGatewayTargetId: - res.data.data.browserGatewayTargetId, + targetId: res.data.data.targetId, siteId: toCreate[i].siteId })); setExistingTargets([...toUpdate, ...newTargets]); diff --git a/src/app/[orgId]/settings/resources/public/create/page.tsx b/src/app/[orgId]/settings/resources/public/create/page.tsx index 407196769..f3715cba0 100644 --- a/src/app/[orgId]/settings/resources/public/create/page.tsx +++ b/src/app/[orgId]/settings/resources/public/create/page.tsx @@ -498,12 +498,13 @@ export default function Page() { if (isNative) { if (nativeSelectedSite) { await api.put( - `/org/${orgId}/resource/${id}/browser-gateway-target`, + `/resource/${id}/target`, { siteId: nativeSelectedSite.siteId, - type: "ssh", - destination: "localhost", - destinationPort: 22 + mode: "ssh", + ip: "localhost", + port: 22, + hcEnabled: false } ); } @@ -516,12 +517,13 @@ export default function Page() { : []; for (const site of sitesToCreate) { await api.put( - `/org/${orgId}/resource/${id}/browser-gateway-target`, + `/resource/${id}/target`, { siteId: site.siteId, - type: "ssh", - destination: bgDestination, - destinationPort: Number(bgDestinationPort) + mode: "ssh", + ip: bgDestination, + port: Number(bgDestinationPort), + hcEnabled: false } ); } @@ -533,12 +535,14 @@ export default function Page() { } else if (resourceType === "rdp" || resourceType === "vnc") { for (const site of bgSelectedSites) { await api.put( - `/org/${orgId}/resource/${id}/browser-gateway-target`, + `/resource/${id}/target`, { siteId: site.siteId, - type: resourceType, - destination: bgDestination, - destinationPort: Number(bgDestinationPort) + mode: resourceType, + ip: bgDestination, + port: Number(bgDestinationPort), + authToken: null, + hcEnabled: false } ); }