Moving to mode replacing http and protocol fields

This commit is contained in:
Owen
2026-05-27 12:04:00 -07:00
parent 464d4990df
commit 06cc13c637
15 changed files with 150 additions and 71 deletions

View File

@@ -129,8 +129,6 @@ export const resources = pgTable("resources", {
ssl: boolean("ssl").notNull().default(false), ssl: boolean("ssl").notNull().default(false),
blockAccess: boolean("blockAccess").notNull().default(false), blockAccess: boolean("blockAccess").notNull().default(false),
sso: boolean("sso").notNull().default(true), sso: boolean("sso").notNull().default(true),
http: boolean("http").notNull().default(true),
protocol: varchar("protocol").notNull(),
proxyPort: integer("proxyPort"), proxyPort: integer("proxyPort"),
emailWhitelistEnabled: boolean("emailWhitelistEnabled") emailWhitelistEnabled: boolean("emailWhitelistEnabled")
.notNull() .notNull()
@@ -159,7 +157,7 @@ export const resources = pgTable("resources", {
postAuthPath: text("postAuthPath"), postAuthPath: text("postAuthPath"),
health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown" health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown"
wildcard: boolean("wildcard").notNull().default(false), 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 }) pamMode: varchar("pamMode", { length: 32 })
.$type<"passthrough" | "push">() .$type<"passthrough" | "push">()
.default("passthrough"), .default("passthrough"),

View File

@@ -142,8 +142,6 @@ export const resources = sqliteTable("resources", {
.notNull() .notNull()
.default(false), .default(false),
sso: integer("sso", { mode: "boolean" }).notNull().default(true), sso: integer("sso", { mode: "boolean" }).notNull().default(true),
http: integer("http", { mode: "boolean" }).notNull().default(true),
protocol: text("protocol").notNull(),
proxyPort: integer("proxyPort"), proxyPort: integer("proxyPort"),
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" }) emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
.notNull() .notNull()
@@ -166,7 +164,6 @@ export const resources = sqliteTable("resources", {
.notNull() .notNull()
.default(false), .default(false),
proxyProtocolVersion: integer("proxyProtocolVersion").default(1), proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
maintenanceModeEnabled: integer("maintenanceModeEnabled", { maintenanceModeEnabled: integer("maintenanceModeEnabled", {
mode: "boolean" mode: "boolean"
}) })
@@ -181,7 +178,7 @@ export const resources = sqliteTable("resources", {
postAuthPath: text("postAuthPath"), postAuthPath: text("postAuthPath"),
health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown" health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown"
wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false), 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") pamMode: text("pamMode")
.$type<"passthrough" | "push">() .$type<"passthrough" | "push">()
.default("passthrough"), .default("passthrough"),

View File

@@ -96,9 +96,7 @@ export async function getTraefikConfig(
resourceName: resources.name, resourceName: resources.name,
fullDomain: resources.fullDomain, fullDomain: resources.fullDomain,
ssl: resources.ssl, ssl: resources.ssl,
http: resources.http,
proxyPort: resources.proxyPort, proxyPort: resources.proxyPort,
protocol: resources.protocol,
subdomain: resources.subdomain, subdomain: resources.subdomain,
domainId: resources.domainId, domainId: resources.domainId,
enabled: resources.enabled, enabled: resources.enabled,
@@ -110,6 +108,7 @@ export async function getTraefikConfig(
proxyProtocol: resources.proxyProtocol, proxyProtocol: resources.proxyProtocol,
proxyProtocolVersion: resources.proxyProtocolVersion, proxyProtocolVersion: resources.proxyProtocolVersion,
wildcard: resources.wildcard, wildcard: resources.wildcard,
mode: resources.mode,
maintenanceModeEnabled: resources.maintenanceModeEnabled, maintenanceModeEnabled: resources.maintenanceModeEnabled,
maintenanceModeType: resources.maintenanceModeType, maintenanceModeType: resources.maintenanceModeType,
@@ -172,8 +171,8 @@ export async function getTraefikConfig(
), ),
inArray(sites.type, siteTypes), inArray(sites.type, siteTypes),
allowRawResources allowRawResources
? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true ? inArray(resources.mode, ["http", "udp", "tcp"]) // allow all three
: eq(resources.http, true) : eq(resources.mode, "http")
) )
) )
.orderBy(desc(targets.priority), targets.targetId); // stable ordering .orderBy(desc(targets.priority), targets.targetId); // stable ordering
@@ -227,9 +226,8 @@ export async function getTraefikConfig(
key: key, key: key,
fullDomain: row.fullDomain, fullDomain: row.fullDomain,
ssl: row.ssl, ssl: row.ssl,
http: row.http,
proxyPort: row.proxyPort, proxyPort: row.proxyPort,
protocol: row.protocol, mode: row.mode,
subdomain: row.subdomain, subdomain: row.subdomain,
domainId: row.domainId, domainId: row.domainId,
enabled: row.enabled, enabled: row.enabled,

View File

@@ -35,16 +35,54 @@ const createResourceParamsSchema = z.strictObject({
orgId: z.string() 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 const createHttpResourceSchema = z
.strictObject({ .strictObject({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
subdomain: z.string().nullable().optional(), subdomain: z.string().nullable().optional(),
http: z.boolean(), http: z.boolean().optional().openapi({
protocol: z.enum(["tcp", "udp"]), 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(), domainId: z.string(),
stickySession: z.boolean().optional(), stickySession: z.boolean().optional(),
postAuthPath: z.string().nullable().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 // SSH Settings
pamMode: z.enum(["passthrough", "push"]).optional(), pamMode: z.enum(["passthrough", "push"]).optional(),
authDaemonPort: z.int().positive().optional(), authDaemonPort: z.int().positive().optional(),
@@ -68,13 +106,27 @@ const createHttpResourceSchema = z
const createRawResourceSchema = z const createRawResourceSchema = z
.strictObject({ .strictObject({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
http: z.boolean(), http: z.boolean().optional().openapi({
protocol: z.enum(["tcp", "udp"]), 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) proxyPort: z.int().min(1).max(65535)
// enableProxy: z.boolean().default(true) // always true now // enableProxy: z.boolean().default(true) // always true now
}) })
.refine( .refine(
(data) => { (data) => {
const resolved = resolveModeFromLegacyFields(data);
if (resolved.error || !resolved.mode) {
return false;
}
if (!config.getRawConfig().flags?.allow_raw_resources) { if (!config.getRawConfig().flags?.allow_raw_resources) {
if (data.proxyPort !== undefined) { if (data.proxyPort !== undefined) {
return false; 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( 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) { if (typeof req.body.proxyPort === "number") {
return await createHttpResource({ req, res, next }, { orgId });
} else {
if ( if (
!config.getRawConfig().flags?.allow_raw_resources && !config.getRawConfig().flags?.allow_raw_resources &&
build == "oss" build == "oss"
@@ -175,6 +228,17 @@ export async function createResource(
} }
return await createRawResource({ req, res, next }, { orgId }); 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) { } catch (error) {
logger.error(error); logger.error(error);
return next( return next(
@@ -210,7 +274,7 @@ async function createHttpResource(
name, name,
domainId, domainId,
postAuthPath, postAuthPath,
browserAccessType, mode,
authDaemonPort, authDaemonPort,
authDaemonMode, authDaemonMode,
pamMode pamMode
@@ -338,12 +402,10 @@ async function createHttpResource(
orgId, orgId,
name, name,
subdomain: finalSubdomain, subdomain: finalSubdomain,
http: true, mode: mode,
browserAccessType: browserAccessType,
pamMode: pamMode, pamMode: pamMode,
authDaemonMode: authDaemonMode, authDaemonMode: authDaemonMode,
authDaemonPort: authDaemonPort, authDaemonPort: authDaemonPort,
protocol: "tcp",
ssl: true, ssl: true,
stickySession: stickySession, stickySession: stickySession,
postAuthPath: postAuthPath, 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; let resource: Resource | undefined;
@@ -438,9 +510,8 @@ async function createRawResource(
niceId, niceId,
orgId, orgId,
name, name,
http, proxyPort,
protocol, mode: resolvedMode.mode
proxyPort
// enableProxy // enableProxy
}) })
.returning(); .returning();

View File

@@ -137,8 +137,6 @@ export type ResourceWithTargets = {
sso: boolean; sso: boolean;
pincodeId: number | null; pincodeId: number | null;
whitelist: boolean; whitelist: boolean;
http: boolean;
protocol: string;
proxyPort: number | null; proxyPort: number | null;
enabled: boolean; enabled: boolean;
domainId: string | null; domainId: string | null;
@@ -146,7 +144,7 @@ export type ResourceWithTargets = {
headerAuthId: number | null; headerAuthId: number | null;
wildcard: boolean; wildcard: boolean;
health: string | null; health: string | null;
browserAccessType: string | null; mode: string | null;
targets: Array<{ targets: Array<{
targetId: number; targetId: number;
ip: string; ip: string;
@@ -175,8 +173,6 @@ function queryResourcesBase() {
sso: resources.sso, sso: resources.sso,
pincodeId: resourcePincode.pincodeId, pincodeId: resourcePincode.pincodeId,
whitelist: resources.emailWhitelistEnabled, whitelist: resources.emailWhitelistEnabled,
http: resources.http,
protocol: resources.protocol,
proxyPort: resources.proxyPort, proxyPort: resources.proxyPort,
enabled: resources.enabled, enabled: resources.enabled,
domainId: resources.domainId, domainId: resources.domainId,
@@ -186,7 +182,7 @@ function queryResourcesBase() {
headerAuthExtendedCompatibilityId: headerAuthExtendedCompatibilityId:
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId, resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
health: resources.health, health: resources.health,
browserAccessType: resources.browserAccessType mode: resources.mode
}) })
.from(resources) .from(resources)
.leftJoin( .leftJoin(
@@ -346,7 +342,9 @@ export async function listResources(
if (typeof authState !== "undefined") { if (typeof authState !== "undefined") {
switch (authState) { switch (authState) {
case "none": case "none":
conditions.push(eq(resources.http, false)); conditions.push(
or(eq(resources.mode, "tcp"), eq(resources.mode, "udp"))
);
break; break;
case "protected": case "protected":
conditions.push( conditions.push(
@@ -525,11 +523,9 @@ export async function listResources(
sso: row.sso, sso: row.sso,
pincodeId: row.pincodeId, pincodeId: row.pincodeId,
whitelist: row.whitelist, whitelist: row.whitelist,
http: row.http,
protocol: row.protocol,
proxyPort: row.proxyPort, proxyPort: row.proxyPort,
wildcard: row.wildcard, wildcard: row.wildcard,
browserAccessType: row.browserAccessType, mode: row.mode,
enabled: row.enabled, enabled: row.enabled,
domainId: row.domainId, domainId: row.domainId,
headerAuthId: row.headerAuthId, headerAuthId: row.headerAuthId,

View File

@@ -72,7 +72,6 @@ const updateHttpResourceBodySchema = z
maintenanceMessage: z.string().max(2000).nullable().optional(), maintenanceMessage: z.string().max(2000).nullable().optional(),
maintenanceEstimatedTime: z.string().max(100).nullable().optional(), maintenanceEstimatedTime: z.string().max(100).nullable().optional(),
postAuthPath: z.string().nullable().optional(), postAuthPath: z.string().nullable().optional(),
browserAccessType: z.enum(["http", "ssh", "rdp", "vnc"]).optional(),
// SSH settings // SSH settings
pamMode: z.enum(["passthrough", "push"]).optional(), pamMode: z.enum(["passthrough", "push"]).optional(),
authDaemonMode: z.enum(["site", "remote", "native"]).optional(), authDaemonMode: z.enum(["site", "remote", "native"]).optional(),

View File

@@ -144,6 +144,21 @@ const createSiteResourceSchema = z
"HTTP mode requires scheme (http or https) and a valid destination port" "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( .refine(
(data) => { (data) => {
return ( return (

View File

@@ -153,6 +153,21 @@ const updateSiteResourceSchema = z
"HTTP mode requires scheme (http or https) and a valid destination port" "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( .refine(
(data) => { (data) => {
return ( return (

View File

@@ -559,7 +559,7 @@ export default function GeneralForm() {
<SettingsContainer> <SettingsContainer>
{resource?.resourceId && {resource?.resourceId &&
resource?.orgId && resource?.orgId &&
resource.browserAccessType == "http" && ( resource.mode == "http" && (
<UptimeAlertSection <UptimeAlertSection
orgId={resource.orgId} orgId={resource.orgId}
resourceId={resource.resourceId} resourceId={resource.resourceId}

View File

@@ -86,8 +86,8 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
href: `/{orgId}/settings/resources/proxy/{niceId}/general` href: `/{orgId}/settings/resources/proxy/{niceId}/general`
}, },
{ {
title: t(`${resource.browserAccessType}Settings`), title: t(`${resource.mode}Settings`),
href: `/{orgId}/settings/resources/proxy/{niceId}/${resource.browserAccessType}` href: `/{orgId}/settings/resources/proxy/{niceId}/${resource.mode}`
} }
]; ];

View File

@@ -149,10 +149,7 @@ export default function ResourceRules(props: {
resolver: zodResolver(addRuleSchema), resolver: zodResolver(addRuleSchema),
defaultValues: { defaultValues: {
action: "ACCEPT", action: "ACCEPT",
match: match: resource.http && resource.mode == "http" ? "PATH" : "IP",
resource.http && resource.browserAccessType == "http"
? "PATH"
: "IP",
value: "" value: ""
} }
}); });
@@ -580,12 +577,11 @@ export default function ResourceRules(props: {
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{resource.http && {resource.http && resource.mode == "http" && (
resource.browserAccessType == "http" && ( <SelectItem value="PATH">
<SelectItem value="PATH"> {RuleMatch.PATH}
{RuleMatch.PATH} </SelectItem>
</SelectItem> )}
)}
<SelectItem value="IP">{RuleMatch.IP}</SelectItem> <SelectItem value="IP">{RuleMatch.IP}</SelectItem>
<SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem> <SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem>
{isMaxmindAvailable && ( {isMaxmindAvailable && (
@@ -1042,7 +1038,7 @@ export default function ResourceRules(props: {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{resource.http && {resource.http &&
resource.browserAccessType == resource.mode ==
"http" && ( "http" && (
<SelectItem value="PATH"> <SelectItem value="PATH">
{ {

View File

@@ -423,7 +423,7 @@ export default function Page() {
: undefined, : undefined,
domainId: httpData.domainId, domainId: httpData.domainId,
protocol: "tcp", protocol: "tcp",
browserAccessType: resourceType, mode: resourceType,
pamMode, pamMode,
authDaemonMode: effectiveMode, authDaemonMode: effectiveMode,
authDaemonPort: effectivePort authDaemonPort: effectivePort

View File

@@ -126,7 +126,7 @@ export default async function ProxyResourcesPage(
fullDomain: resource.fullDomain ?? null, fullDomain: resource.fullDomain ?? null,
ssl: resource.ssl, ssl: resource.ssl,
wildcard: resource.wildcard, wildcard: resource.wildcard,
browserAccessType: resource.browserAccessType, mode: resource.mode,
targets: resource.targets?.map((target) => ({ targets: resource.targets?.map((target) => ({
targetId: target.targetId, targetId: target.targetId,
ip: target.ip, ip: target.ip,

View File

@@ -88,7 +88,7 @@ export type ResourceRow = {
name: string; name: string;
orgId: string; orgId: string;
domain: string; domain: string;
browserAccessType: string | null; mode: string | null;
authState: string; authState: string;
http: boolean; http: boolean;
protocol: string; protocol: string;
@@ -412,10 +412,7 @@ export default function ProxyResourcesTable({
), ),
cell: ({ row }) => { cell: ({ row }) => {
const resourceRow = row.original; const resourceRow = row.original;
if ( if (!resourceRow.http || resourceRow.mode !== "http") {
!resourceRow.http ||
resourceRow.browserAccessType !== "http"
) {
return <span>-</span>; return <span>-</span>;
} }
return ( return (
@@ -446,10 +443,7 @@ export default function ProxyResourcesTable({
header: () => <span className="p-3">{t("uptime30d")}</span>, header: () => <span className="p-3">{t("uptime30d")}</span>,
cell: ({ row }) => { cell: ({ row }) => {
const resourceRow = row.original; const resourceRow = row.original;
if ( if (!resourceRow.http || resourceRow.mode !== "http") {
!resourceRow.http ||
resourceRow.browserAccessType !== "http"
) {
return <span>-</span>; return <span>-</span>;
} }
return ( return (

View File

@@ -36,9 +36,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
resource.fullDomain && resource.fullDomain &&
build != "oss" build != "oss"
); );
const showType = !!(resource.http && resource.browserAccessType); const showType = !!(resource.http && resource.mode);
const showHealth = const showHealth =
!["ssh", "rdp", "vnc"].includes(resource.browserAccessType || "") && !["ssh", "rdp", "vnc"].includes(resource.mode || "") &&
!!resource.health && !!resource.health &&
resource.health !== "unknown"; resource.health !== "unknown";
const showVisibility = !resource.enabled; const showVisibility = !resource.enabled;
@@ -88,7 +88,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
<span className="inline-flex items-center"> <span className="inline-flex items-center">
{resource.browserAccessType!.toUpperCase()} {resource.mode!.toUpperCase()}
</span> </span>
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>