diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 06f204eff..65fac6d98 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -129,8 +129,6 @@ export const resources = pgTable("resources", { ssl: boolean("ssl").notNull().default(false), blockAccess: boolean("blockAccess").notNull().default(false), sso: boolean("sso").notNull().default(true), - http: boolean("http").notNull().default(true), - protocol: varchar("protocol").notNull(), proxyPort: integer("proxyPort"), emailWhitelistEnabled: boolean("emailWhitelistEnabled") .notNull() @@ -159,7 +157,7 @@ export const resources = pgTable("resources", { postAuthPath: text("postAuthPath"), health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown" wildcard: boolean("wildcard").notNull().default(false), - browserAccessType: text("browserAccessType").default("http"), // rdp, ssh, http, vnc + mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc pamMode: varchar("pamMode", { length: 32 }) .$type<"passthrough" | "push">() .default("passthrough"), diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 6455b0599..e7b9755dc 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -142,8 +142,6 @@ export const resources = sqliteTable("resources", { .notNull() .default(false), sso: integer("sso", { mode: "boolean" }).notNull().default(true), - http: integer("http", { mode: "boolean" }).notNull().default(true), - protocol: text("protocol").notNull(), proxyPort: integer("proxyPort"), emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" }) .notNull() @@ -166,7 +164,6 @@ export const resources = sqliteTable("resources", { .notNull() .default(false), proxyProtocolVersion: integer("proxyProtocolVersion").default(1), - maintenanceModeEnabled: integer("maintenanceModeEnabled", { mode: "boolean" }) @@ -181,7 +178,7 @@ export const resources = sqliteTable("resources", { postAuthPath: text("postAuthPath"), health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown" wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false), - browserAccessType: text("browserAccessType").default("http"), // rdp, ssh, http, vnc + mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc pamMode: text("pamMode") .$type<"passthrough" | "push">() .default("passthrough"), diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index b9890582c..c2e18e4b2 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -96,9 +96,7 @@ export async function getTraefikConfig( resourceName: resources.name, fullDomain: resources.fullDomain, ssl: resources.ssl, - http: resources.http, proxyPort: resources.proxyPort, - protocol: resources.protocol, subdomain: resources.subdomain, domainId: resources.domainId, enabled: resources.enabled, @@ -110,6 +108,7 @@ export async function getTraefikConfig( proxyProtocol: resources.proxyProtocol, proxyProtocolVersion: resources.proxyProtocolVersion, wildcard: resources.wildcard, + mode: resources.mode, maintenanceModeEnabled: resources.maintenanceModeEnabled, maintenanceModeType: resources.maintenanceModeType, @@ -172,8 +171,8 @@ export async function getTraefikConfig( ), inArray(sites.type, siteTypes), allowRawResources - ? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true - : eq(resources.http, true) + ? inArray(resources.mode, ["http", "udp", "tcp"]) // allow all three + : eq(resources.mode, "http") ) ) .orderBy(desc(targets.priority), targets.targetId); // stable ordering @@ -227,9 +226,8 @@ export async function getTraefikConfig( key: key, fullDomain: row.fullDomain, ssl: row.ssl, - http: row.http, proxyPort: row.proxyPort, - protocol: row.protocol, + mode: row.mode, subdomain: row.subdomain, domainId: row.domainId, enabled: row.enabled, diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 5c573618e..aeec8b1a9 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -35,16 +35,54 @@ const createResourceParamsSchema = z.strictObject({ orgId: z.string() }); +function resolveModeFromLegacyFields(data: { + mode?: "http" | "ssh" | "rdp" | "vnc" | "tcp" | "udp"; + http?: boolean; + protocol?: "tcp" | "udp"; +}): { + mode?: "http" | "ssh" | "rdp" | "vnc" | "tcp" | "udp"; + error?: string; +} { + if (data.mode) { + return { mode: data.mode }; + } + + if (typeof data.http === "boolean" && data.protocol) { + if (data.http && data.protocol === "tcp") { + return { mode: "http" }; + } + if (!data.http && data.protocol === "tcp") { + return { mode: "tcp" }; + } + if (!data.http && data.protocol === "udp") { + return { mode: "udp" }; + } + return { + error: "Invalid deprecated http/protocol combination" + }; + } + + return { mode: undefined }; +} + const createHttpResourceSchema = z .strictObject({ name: z.string().min(1).max(255), subdomain: z.string().nullable().optional(), - http: z.boolean(), - protocol: z.enum(["tcp", "udp"]), + http: z.boolean().optional().openapi({ + deprecated: true, + description: + "Deprecated. Use `mode` instead. Legacy compatibility only." + }), + protocol: z.enum(["tcp", "udp"]).optional().openapi({ + deprecated: true, + description: + "Deprecated. Use `mode` instead. Legacy compatibility only." + }), domainId: z.string(), stickySession: z.boolean().optional(), postAuthPath: z.string().nullable().optional(), - browserAccessType: z.enum(["http", "ssh", "rdp", "vnc"]).optional(), + mode: z.enum(["http", "ssh", "rdp", "vnc", "tcp", "udp"]).optional(), // SSH Settings pamMode: z.enum(["passthrough", "push"]).optional(), authDaemonPort: z.int().positive().optional(), @@ -68,13 +106,27 @@ const createHttpResourceSchema = z const createRawResourceSchema = z .strictObject({ name: z.string().min(1).max(255), - http: z.boolean(), - protocol: z.enum(["tcp", "udp"]), + http: z.boolean().optional().openapi({ + deprecated: true, + description: + "Deprecated. Use `mode` instead. Legacy compatibility only." + }), + protocol: z.enum(["tcp", "udp"]).optional().openapi({ + deprecated: true, + description: + "Deprecated. Use `mode` instead. Legacy compatibility only." + }), + mode: z.enum(["tcp", "udp"]).optional(), proxyPort: z.int().min(1).max(65535) // enableProxy: z.boolean().default(true) // always true now }) .refine( (data) => { + const resolved = resolveModeFromLegacyFields(data); + if (resolved.error || !resolved.mode) { + return false; + } + if (!config.getRawConfig().flags?.allow_raw_resources) { if (data.proxyPort !== undefined) { return false; @@ -151,17 +203,18 @@ export async function createResource( ); } - if (typeof req.body.http !== "boolean") { + const resolvedMode = resolveModeFromLegacyFields(req.body); + if (resolvedMode.error) { return next( - createHttpError(HttpCode.BAD_REQUEST, "http field is required") + createHttpError(HttpCode.BAD_REQUEST, resolvedMode.error) ); } - const { http } = req.body; + if (resolvedMode.mode) { + req.body.mode = resolvedMode.mode; + } - if (http) { - return await createHttpResource({ req, res, next }, { orgId }); - } else { + if (typeof req.body.proxyPort === "number") { if ( !config.getRawConfig().flags?.allow_raw_resources && build == "oss" @@ -175,6 +228,17 @@ export async function createResource( } return await createRawResource({ req, res, next }, { orgId }); } + + if (req.body.mode) { + return await createHttpResource({ req, res, next }, { orgId }); + } else { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "mode is required when deprecated fields are not provided" + ) + ); + } } catch (error) { logger.error(error); return next( @@ -210,7 +274,7 @@ async function createHttpResource( name, domainId, postAuthPath, - browserAccessType, + mode, authDaemonPort, authDaemonMode, pamMode @@ -338,12 +402,10 @@ async function createHttpResource( orgId, name, subdomain: finalSubdomain, - http: true, - browserAccessType: browserAccessType, + mode: mode, pamMode: pamMode, authDaemonMode: authDaemonMode, authDaemonPort: authDaemonPort, - protocol: "tcp", ssl: true, stickySession: stickySession, postAuthPath: postAuthPath, @@ -425,7 +487,17 @@ async function createRawResource( ); } - const { name, http, protocol, proxyPort } = parsedBody.data; + const { name, proxyPort } = parsedBody.data; + const resolvedMode = resolveModeFromLegacyFields(parsedBody.data); + if (resolvedMode.error || !resolvedMode.mode) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + resolvedMode.error || + "mode is required when deprecated fields are not provided" + ) + ); + } let resource: Resource | undefined; @@ -438,9 +510,8 @@ async function createRawResource( niceId, orgId, name, - http, - protocol, - proxyPort + proxyPort, + mode: resolvedMode.mode // enableProxy }) .returning(); diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 76084076c..1fda68dab 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -137,8 +137,6 @@ export type ResourceWithTargets = { sso: boolean; pincodeId: number | null; whitelist: boolean; - http: boolean; - protocol: string; proxyPort: number | null; enabled: boolean; domainId: string | null; @@ -146,7 +144,7 @@ export type ResourceWithTargets = { headerAuthId: number | null; wildcard: boolean; health: string | null; - browserAccessType: string | null; + mode: string | null; targets: Array<{ targetId: number; ip: string; @@ -175,8 +173,6 @@ function queryResourcesBase() { sso: resources.sso, pincodeId: resourcePincode.pincodeId, whitelist: resources.emailWhitelistEnabled, - http: resources.http, - protocol: resources.protocol, proxyPort: resources.proxyPort, enabled: resources.enabled, domainId: resources.domainId, @@ -186,7 +182,7 @@ function queryResourcesBase() { headerAuthExtendedCompatibilityId: resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId, health: resources.health, - browserAccessType: resources.browserAccessType + mode: resources.mode }) .from(resources) .leftJoin( @@ -346,7 +342,9 @@ export async function listResources( if (typeof authState !== "undefined") { switch (authState) { case "none": - conditions.push(eq(resources.http, false)); + conditions.push( + or(eq(resources.mode, "tcp"), eq(resources.mode, "udp")) + ); break; case "protected": conditions.push( @@ -525,11 +523,9 @@ export async function listResources( sso: row.sso, pincodeId: row.pincodeId, whitelist: row.whitelist, - http: row.http, - protocol: row.protocol, proxyPort: row.proxyPort, wildcard: row.wildcard, - browserAccessType: row.browserAccessType, + mode: row.mode, enabled: row.enabled, domainId: row.domainId, headerAuthId: row.headerAuthId, diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index d799d1994..bd6a9424c 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -72,7 +72,6 @@ const updateHttpResourceBodySchema = z maintenanceMessage: z.string().max(2000).nullable().optional(), maintenanceEstimatedTime: z.string().max(100).nullable().optional(), postAuthPath: z.string().nullable().optional(), - browserAccessType: z.enum(["http", "ssh", "rdp", "vnc"]).optional(), // SSH settings pamMode: z.enum(["passthrough", "push"]).optional(), authDaemonMode: z.enum(["site", "remote", "native"]).optional(), diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index a6f1b352f..25f71df3f 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -144,6 +144,21 @@ const createSiteResourceSchema = z "HTTP mode requires scheme (http or https) and a valid destination port" } ) + .refine( + (data) => { + // destination is only optional for ssh mode with native authDaemonMode + if (data.mode === "ssh" && data.authDaemonMode === "native") { + return true; + } + return ( + data.destination !== undefined && data.destination.trim() !== "" + ); + }, + { + message: + "Destination is required unless mode is ssh with authDaemonMode native" + } + ) .refine( (data) => { return ( diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index f6513b738..4caec7211 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -153,6 +153,21 @@ const updateSiteResourceSchema = z "HTTP mode requires scheme (http or https) and a valid destination port" } ) + .refine( + (data) => { + // destination is only optional for ssh mode with native authDaemonMode + if (data.mode === "ssh" && data.authDaemonMode === "native") { + return true; + } + return ( + data.destination !== undefined && data.destination.trim() !== "" + ); + }, + { + message: + "Destination is required unless mode is ssh with authDaemonMode native" + } + ) .refine( (data) => { return ( diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx index 3ff75cec4..704f6a352 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -559,7 +559,7 @@ export default function GeneralForm() { {resource?.resourceId && resource?.orgId && - resource.browserAccessType == "http" && ( + resource.mode == "http" && ( - {resource.http && - resource.browserAccessType == "http" && ( - - {RuleMatch.PATH} - - )} + {resource.http && resource.mode == "http" && ( + + {RuleMatch.PATH} + + )} {RuleMatch.IP} {RuleMatch.CIDR} {isMaxmindAvailable && ( @@ -1042,7 +1038,7 @@ export default function ResourceRules(props: { {resource.http && - resource.browserAccessType == + resource.mode == "http" && ( { diff --git a/src/app/[orgId]/settings/resources/proxy/create/page.tsx b/src/app/[orgId]/settings/resources/proxy/create/page.tsx index e5a27eff2..7c74be8b7 100644 --- a/src/app/[orgId]/settings/resources/proxy/create/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/create/page.tsx @@ -423,7 +423,7 @@ export default function Page() { : undefined, domainId: httpData.domainId, protocol: "tcp", - browserAccessType: resourceType, + mode: resourceType, pamMode, authDaemonMode: effectiveMode, authDaemonPort: effectivePort diff --git a/src/app/[orgId]/settings/resources/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/page.tsx index 1f43efba7..24b191164 100644 --- a/src/app/[orgId]/settings/resources/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/page.tsx @@ -126,7 +126,7 @@ export default async function ProxyResourcesPage( fullDomain: resource.fullDomain ?? null, ssl: resource.ssl, wildcard: resource.wildcard, - browserAccessType: resource.browserAccessType, + mode: resource.mode, targets: resource.targets?.map((target) => ({ targetId: target.targetId, ip: target.ip, diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index da161a0ec..96767a367 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -88,7 +88,7 @@ export type ResourceRow = { name: string; orgId: string; domain: string; - browserAccessType: string | null; + mode: string | null; authState: string; http: boolean; protocol: string; @@ -412,10 +412,7 @@ export default function ProxyResourcesTable({ ), cell: ({ row }) => { const resourceRow = row.original; - if ( - !resourceRow.http || - resourceRow.browserAccessType !== "http" - ) { + if (!resourceRow.http || resourceRow.mode !== "http") { return -; } return ( @@ -446,10 +443,7 @@ export default function ProxyResourcesTable({ header: () => {t("uptime30d")}, cell: ({ row }) => { const resourceRow = row.original; - if ( - !resourceRow.http || - resourceRow.browserAccessType !== "http" - ) { + if (!resourceRow.http || resourceRow.mode !== "http") { return -; } return ( diff --git a/src/components/ResourceInfoBox.tsx b/src/components/ResourceInfoBox.tsx index a47724595..3e6f11d32 100644 --- a/src/components/ResourceInfoBox.tsx +++ b/src/components/ResourceInfoBox.tsx @@ -36,9 +36,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { resource.fullDomain && build != "oss" ); - const showType = !!(resource.http && resource.browserAccessType); + const showType = !!(resource.http && resource.mode); const showHealth = - !["ssh", "rdp", "vnc"].includes(resource.browserAccessType || "") && + !["ssh", "rdp", "vnc"].includes(resource.mode || "") && !!resource.health && resource.health !== "unknown"; const showVisibility = !resource.enabled; @@ -88,7 +88,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { - {resource.browserAccessType!.toUpperCase()} + {resource.mode!.toUpperCase()}