Sure up some things with browserAccessType

This commit is contained in:
Owen
2026-05-15 17:26:58 -07:00
parent cb75ffc3b7
commit 987b5d580e
8 changed files with 137 additions and 55 deletions

View File

@@ -159,7 +159,8 @@ export const resources = pgTable("resources", {
maintenanceEstimatedTime: text("maintenanceEstimatedTime"), maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
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
}); });
export const targets = pgTable("targets", { export const targets = pgTable("targets", {
@@ -196,9 +197,11 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
onDelete: "cascade" onDelete: "cascade"
}) })
.notNull(), .notNull(),
siteId: integer("siteId").references(() => sites.siteId, { siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade" onDelete: "cascade"
}).notNull(), })
.notNull(),
name: varchar("name"), name: varchar("name"),
hcEnabled: boolean("hcEnabled").notNull().default(false), hcEnabled: boolean("hcEnabled").notNull().default(false),
hcPath: varchar("hcPath"), hcPath: varchar("hcPath"),
@@ -1097,7 +1100,9 @@ export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
complete: boolean("complete").notNull().default(false) complete: boolean("complete").notNull().default(false)
}); });
export const statusHistory = pgTable("statusHistory", { export const statusHistory = pgTable(
"statusHistory",
{
id: serial("id").primaryKey(), id: serial("id").primaryKey(),
entityType: varchar("entityType").notNull(), entityType: varchar("entityType").notNull(),
entityId: integer("entityId").notNull(), entityId: integer("entityId").notNull(),
@@ -1105,11 +1110,20 @@ export const statusHistory = pgTable("statusHistory", {
.notNull() .notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, { onDelete: "cascade" }),
status: varchar("status").notNull(), status: varchar("status").notNull(),
timestamp: integer("timestamp").notNull(), timestamp: integer("timestamp").notNull()
}, (table) => [ },
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp), (table) => [
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp), 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<typeof orgs>; export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>; export type User = InferSelectModel<typeof users>;

View File

@@ -180,7 +180,8 @@ export const resources = sqliteTable("resources", {
maintenanceEstimatedTime: text("maintenanceEstimatedTime"), maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
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
}); });
export const targets = sqliteTable("targets", { export const targets = sqliteTable("targets", {
@@ -219,9 +220,11 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
onDelete: "cascade" onDelete: "cascade"
}) })
.notNull(), .notNull(),
siteId: integer("siteId").references(() => sites.siteId, { siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade" onDelete: "cascade"
}).notNull(), })
.notNull(),
name: text("name"), name: text("name"),
hcEnabled: integer("hcEnabled", { mode: "boolean" }) hcEnabled: integer("hcEnabled", { mode: "boolean" })
.notNull() .notNull()
@@ -1196,7 +1199,9 @@ export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", {
complete: integer("complete", { mode: "boolean" }).notNull().default(false) complete: integer("complete", { mode: "boolean" }).notNull().default(false)
}); });
export const statusHistory = sqliteTable("statusHistory", { export const statusHistory = sqliteTable(
"statusHistory",
{
id: integer("id").primaryKey({ autoIncrement: true }), id: integer("id").primaryKey({ autoIncrement: true }),
entityType: text("entityType").notNull(), // "site" | "healthCheck" entityType: text("entityType").notNull(), // "site" | "healthCheck"
entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId
@@ -1204,11 +1209,20 @@ export const statusHistory = sqliteTable("statusHistory", {
.notNull() .notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, { onDelete: "cascade" }),
status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks
timestamp: integer("timestamp").notNull(), // unix epoch seconds timestamp: integer("timestamp").notNull() // unix epoch seconds
}, (table) => [ },
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp), (table) => [
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp), 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<typeof orgs>; export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>; export type User = InferSelectModel<typeof users>;

View File

@@ -141,6 +141,7 @@ export type ResourceWithTargets = {
headerAuthId: number | null; headerAuthId: number | null;
wildcard: boolean; wildcard: boolean;
health: string | null; health: string | null;
browserAccessType: string | null;
targets: Array<{ targets: Array<{
targetId: number; targetId: number;
ip: string; ip: string;
@@ -178,7 +179,8 @@ function queryResourcesBase() {
headerAuthId: resourceHeaderAuth.headerAuthId, headerAuthId: resourceHeaderAuth.headerAuthId,
headerAuthExtendedCompatibilityId: headerAuthExtendedCompatibilityId:
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId, resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
health: resources.health health: resources.health,
browserAccessType: resources.browserAccessType
}) })
.from(resources) .from(resources)
.leftJoin( .leftJoin(
@@ -478,6 +480,7 @@ export async function listResources(
protocol: row.protocol, protocol: row.protocol,
proxyPort: row.proxyPort, proxyPort: row.proxyPort,
wildcard: row.wildcard, wildcard: row.wildcard,
browserAccessType: row.browserAccessType,
enabled: row.enabled, enabled: row.enabled,
domainId: row.domainId, domainId: row.domainId,
headerAuthId: row.headerAuthId, headerAuthId: row.headerAuthId,

View File

@@ -24,7 +24,10 @@ import {
import { registry } from "@server/openApi"; import { registry } from "@server/openApi";
import { OpenAPITags } from "@server/openApi"; import { OpenAPITags } from "@server/openApi";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; 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 { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
@@ -68,7 +71,8 @@ const updateHttpResourceBodySchema = z
maintenanceTitle: z.string().max(255).nullable().optional(), maintenanceTitle: z.string().max(255).nullable().optional(),
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()
}) })
.refine((data) => Object.keys(data).length > 0, { .refine((data) => Object.keys(data).length > 0, {
error: "At least one field must be provided for update" error: "At least one field must be provided for update"

View File

@@ -507,7 +507,9 @@ export default function GeneralForm() {
name: data.name, name: data.name,
niceId: data.niceId, niceId: data.niceId,
subdomain: data.subdomain subdomain: data.subdomain
? toASCII(finalizeSubdomainSanitize(data.subdomain, true)) ? toASCII(
finalizeSubdomainSanitize(data.subdomain, true)
)
: undefined, : undefined,
domainId: data.domainId, domainId: data.domainId,
proxyPort: data.proxyPort proxyPort: data.proxyPort
@@ -555,7 +557,9 @@ export default function GeneralForm() {
return ( return (
<> <>
<SettingsContainer> <SettingsContainer>
{resource?.resourceId && resource?.orgId && ( {resource?.resourceId &&
resource?.orgId &&
resource.browserAccessType == "http" && (
<UptimeAlertSection <UptimeAlertSection
orgId={resource.orgId} orgId={resource.orgId}
resourceId={resource.resourceId} resourceId={resource.resourceId}

View File

@@ -121,6 +121,10 @@ export default function ReverseProxyTargetsPage(props: {
const params = use(props.params); const params = use(props.params);
const { resource, updateResource } = useResourceContext(); 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( const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery(
resourceQueries.resourceTargets({ resourceQueries.resourceTargets({
resourceId: resource.resourceId resourceId: resource.resourceId
@@ -137,9 +141,12 @@ export default function ReverseProxyTargetsPage(props: {
orgId={params.orgId} orgId={params.orgId}
initialTargets={remoteTargets} initialTargets={remoteTargets}
resource={resource} resource={resource}
targetMode={targetMode}
setTargetMode={setTargetMode}
updateResource={updateResource}
/> />
{resource.http && ( {resource.http && targetMode === "http" && (
<ProxyResourceHttpForm <ProxyResourceHttpForm
resource={resource} resource={resource}
updateResource={updateResource} updateResource={updateResource}
@@ -159,11 +166,17 @@ export default function ReverseProxyTargetsPage(props: {
function ProxyResourceTargetsForm({ function ProxyResourceTargetsForm({
orgId, orgId,
initialTargets, initialTargets,
resource resource,
targetMode,
setTargetMode,
updateResource
}: { }: {
initialTargets: LocalTarget[]; initialTargets: LocalTarget[];
orgId: string; orgId: string;
resource: GetResourceResponse; resource: GetResourceResponse;
targetMode: "http" | "ssh" | "rdp" | "vnc";
setTargetMode: (mode: "http" | "ssh" | "rdp" | "vnc") => void;
updateResource: ResourceContextType["updateResource"];
}) { }) {
const t = useTranslations(); const t = useTranslations();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
@@ -201,9 +214,6 @@ function ProxyResourceTargetsForm({
const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] =
useState<LocalTarget | null>(null); useState<LocalTarget | null>(null);
const [targetMode, setTargetMode] = useState<
"http" | "ssh" | "rdp" | "vnc"
>("http");
const [bgDestination, setBgDestination] = useState(""); const [bgDestination, setBgDestination] = useState("");
const [bgDestinationPort, setBgDestinationPort] = useState(""); const [bgDestinationPort, setBgDestinationPort] = useState("");
const [bgSiteId, setBgSiteId] = useState<number | null>(null); const [bgSiteId, setBgSiteId] = useState<number | null>(null);
@@ -938,11 +948,30 @@ function ProxyResourceTargetsForm({
<span className="text-sm font-medium">Target Type</span> <span className="text-sm font-medium">Target Type</span>
<Select <Select
value={targetMode} value={targetMode}
onValueChange={(v) => onValueChange={async (v) => {
setTargetMode( const mode = v as
v as "http" | "ssh" | "rdp" | "vnc" | "http"
| "ssh"
| "rdp"
| "vnc";
setTargetMode(mode);
try {
await api.post(
`/resource/${resource.resourceId}`,
{ browserAccessType: mode }
);
updateResource({ browserAccessType: mode });
} catch (err) {
toast({
variant: "destructive",
title: t("settingsErrorUpdate"),
description: formatAxiosError(
err,
t("settingsErrorUpdateDescription")
) )
});
} }
}}
> >
<SelectTrigger className="w-36"> <SelectTrigger className="w-36">
<SelectValue /> <SelectValue />

View File

@@ -125,6 +125,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,
targets: resource.targets?.map((target) => ({ targets: resource.targets?.map((target) => ({
targetId: target.targetId, targetId: target.targetId,
ip: target.ip, ip: target.ip,

View File

@@ -82,6 +82,7 @@ export type ResourceRow = {
name: string; name: string;
orgId: string; orgId: string;
domain: string; domain: string;
browserAccessType: string | null;
authState: string; authState: string;
http: boolean; http: boolean;
protocol: string; protocol: string;
@@ -493,6 +494,12 @@ export default function ProxyResourcesTable({
), ),
cell: ({ row }) => { cell: ({ row }) => {
const resourceRow = row.original; const resourceRow = row.original;
if (
!resourceRow.http ||
resourceRow.browserAccessType !== "http"
) {
return <span>-</span>;
}
return ( return (
<TargetStatusCell <TargetStatusCell
targets={resourceRow.targets} targets={resourceRow.targets}
@@ -521,6 +528,12 @@ 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 (
!resourceRow.http ||
resourceRow.browserAccessType !== "http"
) {
return <span>-</span>;
}
return <UptimeMiniBar resourceId={resourceRow.id} days={30} />; return <UptimeMiniBar resourceId={resourceRow.id} days={30} />;
} }
}, },