Trying to get these forms to work

This commit is contained in:
Owen
2026-05-26 21:20:34 -07:00
parent e19b6ebc82
commit cb90672573
11 changed files with 282 additions and 144 deletions

View File

@@ -346,7 +346,7 @@ export const siteResources = pgTable("siteResources", {
niceId: varchar("niceId").notNull(),
name: varchar("name").notNull(),
ssl: boolean("ssl").notNull().default(false),
mode: varchar("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
mode: varchar("mode").$type<"host" | "cidr" | "http" | "ssh">().notNull(), // "host" | "cidr" | "http"
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode

View File

@@ -380,7 +380,7 @@ export const siteResources = sqliteTable("siteResources", {
niceId: text("niceId").notNull(),
name: text("name").notNull(),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
mode: text("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
mode: text("mode").$type<"host" | "cidr" | "http" | "ssh">().notNull(), // "host" | "cidr" | "http"
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode

View File

@@ -44,7 +44,7 @@ const createSiteResourceSchema = z
name: z.string().min(1).max(255),
niceId: z.string().optional(),
// protocol: z.enum(["tcp", "udp"]).optional(),
mode: z.enum(["host", "cidr", "http"]),
mode: z.enum(["host", "cidr", "http", "ssh"]),
ssl: z.boolean().optional(), // only used for http mode
scheme: z.enum(["http", "https"]).optional(),
siteIds: z.array(z.int()).optional(),
@@ -75,7 +75,7 @@ const createSiteResourceSchema = z
.strict()
.refine(
(data) => {
if (data.mode === "host") {
if (data.mode === "host" || data.mode === "ssh") {
// Check if it's a valid IP address using zod (v4 or v6)
const isValidIP = z
// .union([z.ipv4(), z.ipv6()])
@@ -117,13 +117,24 @@ const createSiteResourceSchema = z
)
.refine(
(data) => {
if (data.mode !== "http") return true;
if (data.mode === "http") {
return (
data.scheme !== undefined &&
data.scheme !== null &&
data.destinationPort !== undefined &&
data.destinationPort !== null &&
data.destinationPort >= 1 &&
data.destinationPort <= 65535
);
} else if (data.mode === "ssh") {
// just check the destinationPort
return (
data.destinationPort === undefined ||
(data.destinationPort !== null &&
data.destinationPort >= 1 &&
data.destinationPort <= 65535)
);
}
},
{
message:
@@ -391,6 +402,15 @@ export async function createSiteResource(
);
}
let tcpPortRangeStringAdjusted = tcpPortRangeString;
if (mode === "http") {
tcpPortRangeStringAdjusted = "443,80";
} else if (mode === "ssh") {
tcpPortRangeStringAdjusted = destinationPort
? destinationPort.toString()
: "22";
}
// Create the site resource
const insertValues: typeof siteResources.$inferInsert = {
niceId: updatedNiceId!,
@@ -405,10 +425,12 @@ export async function createSiteResource(
enabled,
alias: alias ? alias.trim() : null,
aliasAddress,
tcpPortRangeString:
mode == "http" ? "443,80" : tcpPortRangeString,
udpPortRangeString: mode == "http" ? "" : udpPortRangeString,
disableIcmp: disableIcmp || (mode == "http" ? true : false), // default to true for http resources, otherwise false
tcpPortRangeString: tcpPortRangeStringAdjusted,
udpPortRangeString:
mode == "http" || mode == "ssh" ? "" : udpPortRangeString,
disableIcmp:
disableIcmp ||
(mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
domainId,
subdomain: finalSubdomain,
fullDomain

View File

@@ -56,7 +56,7 @@ const updateSiteResourceSchema = z
)
.optional(),
// mode: z.enum(["host", "cidr", "port"]).optional(),
mode: z.enum(["host", "cidr", "http"]).optional(),
mode: z.enum(["host", "cidr", "http", "ssh"]).optional(),
ssl: z.boolean().optional(),
scheme: z.enum(["http", "https"]).nullish(),
// proxyPort: z.int().positive().nullish(),
@@ -85,7 +85,10 @@ const updateSiteResourceSchema = z
.strict()
.refine(
(data) => {
if (data.mode === "host" && data.destination) {
if (
(data.mode === "host" || data.mode == "ssh") &&
data.destination
) {
const isValidIP = z
// .union([z.ipv4(), z.ipv6()])
.union([z.ipv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere
@@ -126,7 +129,7 @@ const updateSiteResourceSchema = z
)
.refine(
(data) => {
if (data.mode !== "http") return true;
if (data.mode === "http") {
return (
data.scheme !== undefined &&
data.scheme !== null &&
@@ -135,6 +138,15 @@ const updateSiteResourceSchema = z
data.destinationPort >= 1 &&
data.destinationPort <= 65535
);
} else if (data.mode === "ssh") {
// just check the destinationPort
return (
data.destinationPort === undefined ||
(data.destinationPort !== null &&
data.destinationPort >= 1 &&
data.destinationPort <= 65535)
);
}
},
{
message:
@@ -446,6 +458,16 @@ export async function updateSiteResource(
})
}
: {};
let tcpPortRangeStringAdjusted = tcpPortRangeString;
if (mode === "http") {
tcpPortRangeStringAdjusted = "443,80";
} else if (mode === "ssh") {
tcpPortRangeStringAdjusted = destinationPort
? destinationPort.toString()
: "22";
}
[updatedSiteResource] = await trx
.update(siteResources)
.set({
@@ -458,12 +480,14 @@ export async function updateSiteResource(
destinationPort,
enabled,
alias: alias ? alias.trim() : null,
tcpPortRangeString:
mode == "http" ? "443,80" : tcpPortRangeString,
tcpPortRangeString: tcpPortRangeStringAdjusted,
udpPortRangeString:
mode == "http" ? "" : udpPortRangeString,
mode == "http" || mode == "ssh"
? ""
: udpPortRangeString,
disableIcmp:
disableIcmp || (mode == "http" ? true : false), // default to true for http resources, otherwise false
disableIcmp ||
(mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
domainId,
subdomain: finalSubdomain,
fullDomain,

View File

@@ -120,7 +120,7 @@ export default async function ClientResourcesPage(
// proxyPort: siteResource.proxyPort,
siteIds: siteResource.siteIds,
destination: siteResource.destination,
httpHttpsPort: siteResource.destinationPort ?? null,
destinationPort: siteResource.destinationPort ?? null,
alias: siteResource.alias || null,
aliasAddress: siteResource.aliasAddress || null,
siteNiceIds: siteResource.siteNiceIds,

View File

@@ -83,7 +83,7 @@ export type InternalResourceRow = {
// protocol: string | null;
// proxyPort: number | null;
destination: string;
httpHttpsPort: number | null;
destinationPort: number | null;
alias: string | null;
aliasAddress: string | null;
niceId: string;
@@ -107,7 +107,7 @@ function formatDestinationDisplay(row: InternalResourceRow): string {
return formatSiteResourceDestinationDisplay({
mode: row.mode,
destination: row.destination,
httpHttpsPort: row.httpHttpsPort,
destinationPort: row.destinationPort,
scheme: row.scheme
});
}

View File

@@ -76,7 +76,7 @@ export default function CreateInternalResourceDialog({
...(data.mode === "http" && {
scheme: data.scheme,
ssl: data.ssl ?? false,
destinationPort: data.httpHttpsPort ?? undefined,
destinationPort: data.destinationPort ?? undefined,
domainId: data.httpConfigDomainId
? data.httpConfigDomainId
: undefined,

View File

@@ -78,7 +78,7 @@ export default function EditInternalResourceDialog({
...(data.mode === "http" && {
scheme: data.scheme,
ssl: data.ssl ?? false,
destinationPort: data.httpHttpsPort ?? null,
destinationPort: data.destinationPort ?? null,
domainId: data.httpConfigDomainId
? data.httpConfigDomainId
: undefined,

View File

@@ -54,6 +54,7 @@ import {
MultiSitesSelector,
formatMultiSitesSelectorLabel
} from "./multi-site-selector";
import { SitesSelector } from "./site-selector";
import type { Selectedsite } from "./site-selector";
import { MachinesSelector } from "./machines-selector";
@@ -154,7 +155,7 @@ export type InternalResourceData = {
authDaemonMode?: "site" | "remote" | "native" | null;
authDaemonPort?: number | null;
pamMode?: "passthrough" | "push" | null;
httpHttpsPort?: number | null;
destinationPort?: number | null;
scheme?: "http" | "https" | null;
ssl?: boolean;
subdomain?: string | null;
@@ -187,7 +188,7 @@ export type InternalResourceFormValues = {
authDaemonMode?: "site" | "remote" | "native" | null;
authDaemonPort?: number | null;
pamMode?: "passthrough" | "push" | null;
httpHttpsPort?: number | null;
destinationPort?: number | null;
scheme?: "http" | "https";
ssl?: boolean;
httpConfigSubdomain?: string | null;
@@ -286,7 +287,7 @@ export function InternalResourceForm({
variant === "create"
? "createInternalResourceDialogAlias"
: "editInternalResourceDialogAlias";
const httpHttpsPortLabelKey =
const destinationPortLabelKey =
variant === "create"
? "createInternalResourceDialogModePort"
: "editInternalResourceDialogModePort";
@@ -308,16 +309,9 @@ export function InternalResourceForm({
name: z.string().min(1, t(nameRequiredKey)).max(255, t(nameMaxKey)),
siteIds: siteIdsSchema,
mode: z.enum(["host", "cidr", "http", "ssh"]),
destination: z
.string()
.min(
1,
destinationRequiredKey
? { message: t(destinationRequiredKey) }
: undefined
),
destination: z.string(),
alias: z.string().nullish(),
httpHttpsPort: z
destinationPort: z
.number()
.int()
.min(1)
@@ -356,6 +350,20 @@ export function InternalResourceForm({
.optional()
})
.superRefine((data, ctx) => {
const isNativeSsh =
data.mode === "ssh" && data.authDaemonMode === "native";
if (
!isNativeSsh &&
(!data.destination || data.destination.length < 1)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: destinationRequiredKey
? t(destinationRequiredKey)
: "Destination is required",
path: ["destination"]
});
}
if (data.mode !== "http") return;
if (!data.scheme) {
ctx.addIssue({
@@ -365,14 +373,15 @@ export function InternalResourceForm({
});
}
if (
data.httpHttpsPort == null ||
!Number.isFinite(data.httpHttpsPort) ||
data.httpHttpsPort < 1
!isNativeSsh &&
(data.destinationPort == null ||
!Number.isFinite(data.destinationPort) ||
data.destinationPort < 1)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("internalResourceHttpPortRequired"),
path: ["httpHttpsPort"]
path: ["destinationPort"]
});
}
});
@@ -523,7 +532,7 @@ export function InternalResourceForm({
: (resource.authDaemonMode ?? "site"),
authDaemonPort: resource.authDaemonPort ?? null,
pamMode: resource.pamMode ?? "passthrough",
httpHttpsPort: resource.httpHttpsPort ?? null,
destinationPort: resource.destinationPort ?? null,
scheme: resource.scheme ?? "http",
ssl: resource.ssl ?? false,
httpConfigSubdomain: resource.subdomain ?? null,
@@ -540,7 +549,7 @@ export function InternalResourceForm({
mode: "host",
destination: "",
alias: null,
httpHttpsPort: null,
destinationPort: null,
scheme: "http",
ssl: true,
httpConfigSubdomain: null,
@@ -605,7 +614,7 @@ export function InternalResourceForm({
mode: "host",
destination: "",
alias: null,
httpHttpsPort: null,
destinationPort: null,
scheme: "http",
ssl: true,
httpConfigSubdomain: null,
@@ -641,7 +650,7 @@ export function InternalResourceForm({
mode: resource.mode ?? "host",
destination: resource.destination ?? "",
alias: resource.alias ?? null,
httpHttpsPort: resource.httpHttpsPort ?? null,
destinationPort: resource.destinationPort ?? null,
scheme: resource.scheme ?? "http",
ssl: resource.ssl ?? false,
httpConfigSubdomain: resource.subdomain ?? null,
@@ -812,8 +821,69 @@ export function InternalResourceForm({
<FormLabel>
{t("sites")}
</FormLabel>
{mode === "ssh" &&
sshServerMode ===
"native" ? (
<Popover>
<PopoverTrigger asChild>
<PopoverTrigger
asChild
>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between",
selectedSites.length ===
0 &&
"text-muted-foreground"
)}
>
<span className="truncate text-left">
{selectedSites[0]
?.name ??
t(
"selectSite"
)}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<SitesSelector
orgId={
orgId
}
selectedSite={
selectedSites[0] ??
null
}
filterTypes={[
"newt"
]}
onSelectSite={(
site
) => {
setSelectedSites(
[
site
]
);
field.onChange(
[
site.siteId
]
);
}}
/>
</PopoverContent>
</Popover>
) : (
<Popover>
<PopoverTrigger
asChild
>
<FormControl>
<Button
variant="outline"
@@ -837,7 +907,9 @@ export function InternalResourceForm({
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<MultiSitesSelector
orgId={orgId}
orgId={
orgId
}
selectedSites={
selectedSites
}
@@ -862,6 +934,7 @@ export function InternalResourceForm({
/>
</PopoverContent>
</Popover>
)}
<FormMessage />
</FormItem>
)}
@@ -950,8 +1023,10 @@ export function InternalResourceForm({
"grid gap-4 items-start",
mode === "cidr" && "grid-cols-1",
mode === "http" && "grid-cols-3",
(mode === "host" || mode === "ssh") &&
"grid-cols-2"
mode === "host" && "grid-cols-2",
mode === "ssh" &&
sshServerMode !== "native" &&
"grid-cols-3"
)}
>
{mode === "http" && (
@@ -996,7 +1071,11 @@ export function InternalResourceForm({
/>
</div>
)}
{sshServerMode !== "native" && (
{((mode === "ssh" &&
sshServerMode !== "native") ||
mode === "http" ||
mode === "host" ||
mode === "cidr") && (
<div
className={cn(
mode === "cidr" && "col-span-1",
@@ -1030,8 +1109,9 @@ export function InternalResourceForm({
/>
</div>
)}
{(mode === "host" || mode === "ssh") &&
sshServerMode !== "native" && (
{(mode === "host" ||
(mode === "ssh" &&
sshServerMode !== "native")) && (
<div className="min-w-0">
<FormField
control={form.control}
@@ -1057,16 +1137,18 @@ export function InternalResourceForm({
/>
</div>
)}
{mode === "http" && (
{(mode === "http" ||
(mode === "ssh" &&
sshServerMode !== "native")) && (
<div className="min-w-0">
<FormField
control={form.control}
name="httpHttpsPort"
name="destinationPort"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
httpHttpsPortLabelKey
destinationPortLabelKey
)}
</FormLabel>
<FormControl>
@@ -1690,6 +1772,16 @@ export function InternalResourceForm({
"authDaemonPort",
null
);
// Trim to single site
if (selectedSites.length > 1) {
const first =
selectedSites.slice(0, 1);
setSelectedSites(first);
form.setValue(
"siteIds",
first.map((s) => s.siteId)
);
}
} else {
form.setValue(
"authDaemonMode",

View File

@@ -73,7 +73,7 @@ function PrivateResourceMeta({ row }: { row: SiteResourceRow }) {
const dest = formatSiteResourceDestinationDisplay({
mode: row.mode,
destination: row.destination,
httpHttpsPort: row.destinationPort ?? null,
destinationPort: row.destinationPort ?? null,
scheme: row.scheme
});
return (
@@ -149,7 +149,7 @@ function PrivateAccessMethod({ row }: { row: SiteResourceRow }) {
const dest = formatSiteResourceDestinationDisplay({
mode: row.mode,
destination: row.destination,
httpHttpsPort: row.destinationPort,
destinationPort: row.destinationPort,
scheme: row.scheme
});
return (

View File

@@ -1,16 +1,16 @@
export type SiteResourceDestinationInput = {
mode: "host" | "cidr" | "http";
destination: string;
httpHttpsPort: number | null;
destinationPort: number | null;
scheme: "http" | "https" | null;
};
export function resolveHttpHttpsDisplayPort(
mode: "http",
httpHttpsPort: number | null
destinationPort: number | null
): number {
if (httpHttpsPort != null) {
return httpHttpsPort;
if (destinationPort != null) {
return destinationPort;
}
return 80;
}
@@ -18,11 +18,11 @@ export function resolveHttpHttpsDisplayPort(
export function formatSiteResourceDestinationDisplay(
row: SiteResourceDestinationInput
): string {
const { mode, destination, httpHttpsPort, scheme } = row;
const { mode, destination, destinationPort, scheme } = row;
if (mode !== "http") {
return destination;
}
const port = resolveHttpHttpsDisplayPort(mode, httpHttpsPort);
const port = resolveHttpHttpsDisplayPort(mode, destinationPort);
const downstreamScheme = scheme ?? "http";
const hostPart =
destination.includes(":") && !destination.startsWith("[")