diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 7fbcef621..cadeb7eeb 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -159,7 +159,8 @@ export const resources = pgTable("resources", { maintenanceEstimatedTime: text("maintenanceEstimatedTime"), postAuthPath: text("postAuthPath"), 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 }); export const targets = pgTable("targets", { @@ -196,9 +197,11 @@ export const targetHealthCheck = pgTable("targetHealthCheck", { onDelete: "cascade" }) .notNull(), - siteId: integer("siteId").references(() => sites.siteId, { - onDelete: "cascade" - }).notNull(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), name: varchar("name"), hcEnabled: boolean("hcEnabled").notNull().default(false), hcPath: varchar("hcPath"), @@ -1097,19 +1100,30 @@ export const roundTripMessageTracker = pgTable("roundTripMessageTracker", { complete: boolean("complete").notNull().default(false) }); -export const statusHistory = pgTable("statusHistory", { - id: serial("id").primaryKey(), - entityType: varchar("entityType").notNull(), - entityId: integer("entityId").notNull(), - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - status: varchar("status").notNull(), - timestamp: integer("timestamp").notNull(), -}, (table) => [ - index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp), - index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp), -]); +export const statusHistory = pgTable( + "statusHistory", + { + id: serial("id").primaryKey(), + entityType: varchar("entityType").notNull(), + entityId: integer("entityId").notNull(), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + status: varchar("status").notNull(), + timestamp: integer("timestamp").notNull() + }, + (table) => [ + index("idx_statusHistory_entity").on( + table.entityType, + table.entityId, + table.timestamp + ), + index("idx_statusHistory_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); export type Org = InferSelectModel; export type User = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 423190420..99731339c 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -180,7 +180,8 @@ export const resources = sqliteTable("resources", { maintenanceEstimatedTime: text("maintenanceEstimatedTime"), postAuthPath: text("postAuthPath"), 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 }); export const targets = sqliteTable("targets", { @@ -219,9 +220,11 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", { onDelete: "cascade" }) .notNull(), - siteId: integer("siteId").references(() => sites.siteId, { - onDelete: "cascade" - }).notNull(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), name: text("name"), hcEnabled: integer("hcEnabled", { mode: "boolean" }) .notNull() @@ -1196,19 +1199,30 @@ export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", { complete: integer("complete", { mode: "boolean" }).notNull().default(false) }); -export const statusHistory = sqliteTable("statusHistory", { - id: integer("id").primaryKey({ autoIncrement: true }), - entityType: text("entityType").notNull(), // "site" | "healthCheck" - entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks - timestamp: integer("timestamp").notNull(), // unix epoch seconds -}, (table) => [ - index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp), - index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp), -]); +export const statusHistory = sqliteTable( + "statusHistory", + { + id: integer("id").primaryKey({ autoIncrement: true }), + entityType: text("entityType").notNull(), // "site" | "healthCheck" + entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks + timestamp: integer("timestamp").notNull() // unix epoch seconds + }, + (table) => [ + index("idx_statusHistory_entity").on( + table.entityType, + table.entityId, + table.timestamp + ), + index("idx_statusHistory_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); export type Org = InferSelectModel; export type User = InferSelectModel; diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index e76fbbf05..c63bd965e 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -141,6 +141,7 @@ export type ResourceWithTargets = { headerAuthId: number | null; wildcard: boolean; health: string | null; + browserAccessType: string | null; targets: Array<{ targetId: number; ip: string; @@ -178,7 +179,8 @@ function queryResourcesBase() { headerAuthId: resourceHeaderAuth.headerAuthId, headerAuthExtendedCompatibilityId: resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId, - health: resources.health + health: resources.health, + browserAccessType: resources.browserAccessType }) .from(resources) .leftJoin( @@ -478,6 +480,7 @@ export async function listResources( protocol: row.protocol, proxyPort: row.proxyPort, wildcard: row.wildcard, + browserAccessType: row.browserAccessType, enabled: row.enabled, domainId: row.domainId, headerAuthId: row.headerAuthId, diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 0a7052dce..0cb8617aa 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -24,7 +24,10 @@ import { import { registry } from "@server/openApi"; import { OpenAPITags } from "@server/openApi"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; -import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils"; +import { + validateAndConstructDomain, + checkWildcardDomainConflict +} from "@server/lib/domainUtils"; import { build } from "@server/build"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; @@ -68,7 +71,8 @@ const updateHttpResourceBodySchema = z maintenanceTitle: z.string().max(255).nullable().optional(), maintenanceMessage: z.string().max(2000).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() }) .refine((data) => Object.keys(data).length > 0, { error: "At least one field must be provided for update" 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 62a6b9fed..11c347f36 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -507,7 +507,9 @@ export default function GeneralForm() { name: data.name, niceId: data.niceId, subdomain: data.subdomain - ? toASCII(finalizeSubdomainSanitize(data.subdomain, true)) + ? toASCII( + finalizeSubdomainSanitize(data.subdomain, true) + ) : undefined, domainId: data.domainId, proxyPort: data.proxyPort @@ -555,13 +557,15 @@ export default function GeneralForm() { return ( <> - {resource?.resourceId && resource?.orgId && ( - - )} + {resource?.resourceId && + resource?.orgId && + resource.browserAccessType == "http" && ( + + )} diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx index 2ac4eafe4..7415085f9 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx @@ -121,6 +121,10 @@ export default function ReverseProxyTargetsPage(props: { const params = use(props.params); const { resource, updateResource } = useResourceContext(); + const [targetMode, setTargetMode] = useState< + "http" | "ssh" | "rdp" | "vnc" + >((resource.browserAccessType as "http" | "ssh" | "rdp" | "vnc") || "http"); + const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery( resourceQueries.resourceTargets({ resourceId: resource.resourceId @@ -137,9 +141,12 @@ export default function ReverseProxyTargetsPage(props: { orgId={params.orgId} initialTargets={remoteTargets} resource={resource} + targetMode={targetMode} + setTargetMode={setTargetMode} + updateResource={updateResource} /> - {resource.http && ( + {resource.http && targetMode === "http" && ( void; + updateResource: ResourceContextType["updateResource"]; }) { const t = useTranslations(); const api = createApiClient(useEnvContext()); @@ -201,9 +214,6 @@ function ProxyResourceTargetsForm({ const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = useState(null); - const [targetMode, setTargetMode] = useState< - "http" | "ssh" | "rdp" | "vnc" - >("http"); const [bgDestination, setBgDestination] = useState(""); const [bgDestinationPort, setBgDestinationPort] = useState(""); const [bgSiteId, setBgSiteId] = useState(null); @@ -938,11 +948,30 @@ function ProxyResourceTargetsForm({ Target Type