Add tcp and udp specific pages

This commit is contained in:
Owen
2026-06-01 16:05:20 -07:00
parent 51bb149fd5
commit 605dd2f3c9
6 changed files with 442 additions and 209 deletions

View File

@@ -3430,5 +3430,7 @@
"memberPortalShowingResources": "Showing {start}-{end} of {total} resources",
"memberPortalPrevious": "Previous",
"memberPortalNext": "Next",
"httpSettings": "HTTP Settings"
"httpSettings": "HTTP Settings",
"tcpSettings": "TCP Settings",
"udpSettings": "UDP Settings"
}

View File

@@ -519,6 +519,46 @@ export default async function migration() {
`);
}
const existingRoleResources = await db.execute(sql`
SELECT "roleId"
FROM "roleResources"
WHERE "resourceId" = ${resource.resourceId}
`);
for (const roleRow of existingRoleResources.rows as {
roleId: number;
}[]) {
await db.execute(sql`
INSERT INTO "rolePolicies" ("roleId", "resourcePolicyId")
SELECT ${roleRow.roleId}, ${resourcePolicyId}
WHERE NOT EXISTS (
SELECT 1
FROM "rolePolicies"
WHERE "roleId" = ${roleRow.roleId}
AND "resourcePolicyId" = ${resourcePolicyId}
)
`);
}
const existingUserResources = await db.execute(sql`
SELECT "userId"
FROM "userResources"
WHERE "resourceId" = ${resource.resourceId}
`);
for (const userRow of existingUserResources.rows as {
userId: string;
}[]) {
await db.execute(sql`
INSERT INTO "userPolicies" ("userId", "resourcePolicyId")
SELECT ${userRow.userId}, ${resourcePolicyId}
WHERE NOT EXISTS (
SELECT 1
FROM "userPolicies"
WHERE "userId" = ${userRow.userId}
AND "resourcePolicyId" = ${resourcePolicyId}
)
`);
}
await db.execute(sql`
DELETE FROM "resourcePincode"
WHERE "resourceId" = ${resource.resourceId}

View File

@@ -32,6 +32,8 @@ export function generateName(): string {
return name.replace(/[^a-z0-9-]/g, "");
}
await migration();
export default async function migration() {
console.log(`Running setup script ${version}...`);
@@ -456,6 +458,42 @@ export default async function migration() {
) VALUES (?, ?)`
);
const selectRoleResources = db.prepare(
`SELECT "roleId"
FROM 'roleResources'
WHERE "resourceId" = ?`
);
const rolePolicyExists = db.prepare(
`SELECT 1
FROM 'rolePolicies'
WHERE "roleId" = ? AND "resourcePolicyId" = ?
LIMIT 1`
);
const insertRolePolicy = db.prepare(
`INSERT INTO 'rolePolicies' (
"roleId",
"resourcePolicyId"
) VALUES (?, ?)`
);
const selectUserResources = db.prepare(
`SELECT "userId"
FROM 'userResources'
WHERE "resourceId" = ?`
);
const userPolicyExists = db.prepare(
`SELECT 1
FROM 'userPolicies'
WHERE "userId" = ? AND "resourcePolicyId" = ?
LIMIT 1`
);
const insertUserPolicy = db.prepare(
`INSERT INTO 'userPolicies' (
"userId",
"resourcePolicyId"
) VALUES (?, ?)`
);
const deleteResourcePincodes = db.prepare(
`DELETE FROM 'resourcePincode' WHERE "resourceId" = ?`
);
@@ -586,6 +624,32 @@ export default async function migration() {
);
}
const resourceRoles = selectRoleResources.all(
resource.resourceId
) as { roleId: number }[];
for (const role of resourceRoles) {
const exists = rolePolicyExists.get(
role.roleId,
policyId
) as { 1: number } | undefined;
if (!exists) {
insertRolePolicy.run(role.roleId, policyId);
}
}
const resourceUsers = selectUserResources.all(
resource.resourceId
) as { userId: string }[];
for (const user of resourceUsers) {
const exists = userPolicyExists.get(
user.userId,
policyId
) as { 1: number } | undefined;
if (!exists) {
insertUserPolicy.run(user.userId, policyId);
}
}
deleteResourcePincodes.run(resource.resourceId);
deleteResourcePasswords.run(resource.resourceId);
deleteResourceHeaderAuth.run(resource.resourceId);

View File

@@ -101,12 +101,6 @@ export default function ReverseProxyTargetsPage(props: {
/>
)}
{resource.mode == "tcp" && (
<ProxyResourceProtocolForm
resource={resource}
updateResource={updateResource}
/>
)}
</SettingsContainer>
);
}
@@ -405,205 +399,4 @@ function ProxyResourceHttpForm({
</SettingsSectionBody>
</SettingsSection>
);
}
function ProxyResourceProtocolForm({
resource,
updateResource
}: Pick<ResourceContextType, "resource" | "updateResource">) {
const t = useTranslations();
const api = createApiClient(useEnvContext());
const proxySettingsSchema = z.object({
setHostHeader: z
.string()
.optional()
.refine(
(data) => {
if (data) {
return tlsNameSchema.safeParse(data).success;
}
return true;
},
{
message: t("proxyErrorInvalidHeader")
}
),
headers: z
.array(z.object({ name: z.string(), value: z.string() }))
.nullable(),
proxyProtocol: z.boolean().optional(),
proxyProtocolVersion: z.int().min(1).max(2).optional()
});
const proxySettingsForm = useForm({
resolver: zodResolver(proxySettingsSchema),
defaultValues: {
setHostHeader: resource.setHostHeader || "",
headers: resource.headers,
proxyProtocol: resource.proxyProtocol || false,
proxyProtocolVersion: resource.proxyProtocolVersion || 1
}
});
const router = useRouter();
const [, formAction, isSubmitting] = useActionState(
saveProtocolSettings,
null
);
async function saveProtocolSettings() {
const isValid = proxySettingsForm.trigger();
if (!isValid) return;
try {
// For TCP/UDP resources, save proxy protocol settings
const proxyData = proxySettingsForm.getValues();
const payload = {
proxyProtocol: proxyData.proxyProtocol || false,
proxyProtocolVersion: proxyData.proxyProtocolVersion || 1
};
await api.post(`/resource/${resource.resourceId}`, payload);
updateResource({
...resource,
proxyProtocol: proxyData.proxyProtocol || false,
proxyProtocolVersion: proxyData.proxyProtocolVersion || 1
});
toast({
title: t("settingsUpdated"),
description: t("settingsUpdatedDescription")
});
router.refresh();
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: t("settingsErrorUpdate"),
description: formatAxiosError(
err,
t("settingsErrorUpdateDescription")
)
});
}
}
return (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("proxyProtocol")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("proxyProtocolDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...proxySettingsForm}>
<form
action={formAction}
className="space-y-4"
id="proxy-protocol-settings-form"
>
<FormField
control={proxySettingsForm.control}
name="proxyProtocol"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="proxy-protocol-toggle"
label={t("enableProxyProtocol")}
description={t(
"proxyProtocolInfo"
)}
defaultChecked={
field.value || false
}
onCheckedChange={(val) => {
field.onChange(val);
}}
/>
</FormControl>
</FormItem>
)}
/>
{proxySettingsForm.watch("proxyProtocol") && (
<>
<FormField
control={proxySettingsForm.control}
name="proxyProtocolVersion"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("proxyProtocolVersion")}
</FormLabel>
<FormControl>
<Select
value={String(
field.value || 1
)}
onValueChange={(
value
) =>
field.onChange(
parseInt(
value,
10
)
)
}
>
<SelectTrigger>
<SelectValue placeholder="Select version" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">
{t("version1")}
</SelectItem>
<SelectItem value="2">
{t("version2")}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription>
{t("versionDescription")}
</FormDescription>
</FormItem>
)}
/>
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>{t("warning")}:</strong>{" "}
{t("proxyProtocolWarning")}
</AlertDescription>
</Alert>
</>
)}
</form>
</Form>
</SettingsSectionForm>
<form action={formAction} className="flex justify-end">
<Button
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
>
{t("saveProxyProtocol")}
</Button>
</form>
</SettingsSectionBody>
</SettingsSection>
);
}
}

View File

@@ -0,0 +1,291 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select";
import {
SettingsContainer,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import type { ResourceContextType } from "@app/contexts/resourceContext";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
import { resourceQueries } from "@app/lib/queries";
import { zodResolver } from "@hookform/resolvers/zod";
import { tlsNameSchema } from "@server/lib/schemas";
import { useQuery } from "@tanstack/react-query";
import {
ProxyResourceTargetsForm
} from "@app/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm";
import {
AlertTriangle,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import {
use,
useActionState,
} from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
export default function ReverseProxyTargetsPage(props: {
params: Promise<{ resourceId: number; orgId: string }>;
}) {
const params = use(props.params);
const { resource, updateResource } = useResourceContext();
const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery(
resourceQueries.resourceTargets({
resourceId: resource.resourceId
})
);
if (isLoadingTargets) {
return null;
}
return (
<SettingsContainer>
<ProxyResourceTargetsForm
orgId={params.orgId}
isHttp={["http", "ssh", "rdp", "vnc"].includes(resource.mode)}
initialTargets={remoteTargets}
resource={resource}
updateResource={updateResource}
/>
{resource.mode == "tcp" && (
<ProxyResourceProtocolForm
resource={resource}
updateResource={updateResource}
/>
)}
</SettingsContainer>
);
}
function ProxyResourceProtocolForm({
resource,
updateResource
}: Pick<ResourceContextType, "resource" | "updateResource">) {
const t = useTranslations();
const api = createApiClient(useEnvContext());
const proxySettingsSchema = z.object({
setHostHeader: z
.string()
.optional()
.refine(
(data) => {
if (data) {
return tlsNameSchema.safeParse(data).success;
}
return true;
},
{
message: t("proxyErrorInvalidHeader")
}
),
headers: z
.array(z.object({ name: z.string(), value: z.string() }))
.nullable(),
proxyProtocol: z.boolean().optional(),
proxyProtocolVersion: z.int().min(1).max(2).optional()
});
const proxySettingsForm = useForm({
resolver: zodResolver(proxySettingsSchema),
defaultValues: {
setHostHeader: resource.setHostHeader || "",
headers: resource.headers,
proxyProtocol: resource.proxyProtocol || false,
proxyProtocolVersion: resource.proxyProtocolVersion || 1
}
});
const router = useRouter();
const [, formAction, isSubmitting] = useActionState(
saveProtocolSettings,
null
);
async function saveProtocolSettings() {
const isValid = proxySettingsForm.trigger();
if (!isValid) return;
try {
// For TCP/UDP resources, save proxy protocol settings
const proxyData = proxySettingsForm.getValues();
const payload = {
proxyProtocol: proxyData.proxyProtocol || false,
proxyProtocolVersion: proxyData.proxyProtocolVersion || 1
};
await api.post(`/resource/${resource.resourceId}`, payload);
updateResource({
...resource,
proxyProtocol: proxyData.proxyProtocol || false,
proxyProtocolVersion: proxyData.proxyProtocolVersion || 1
});
toast({
title: t("settingsUpdated"),
description: t("settingsUpdatedDescription")
});
router.refresh();
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: t("settingsErrorUpdate"),
description: formatAxiosError(
err,
t("settingsErrorUpdateDescription")
)
});
}
}
return (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("proxyProtocol")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("proxyProtocolDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...proxySettingsForm}>
<form
action={formAction}
className="space-y-4"
id="proxy-protocol-settings-form"
>
<FormField
control={proxySettingsForm.control}
name="proxyProtocol"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="proxy-protocol-toggle"
label={t("enableProxyProtocol")}
description={t(
"proxyProtocolInfo"
)}
defaultChecked={
field.value || false
}
onCheckedChange={(val) => {
field.onChange(val);
}}
/>
</FormControl>
</FormItem>
)}
/>
{proxySettingsForm.watch("proxyProtocol") && (
<>
<FormField
control={proxySettingsForm.control}
name="proxyProtocolVersion"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("proxyProtocolVersion")}
</FormLabel>
<FormControl>
<Select
value={String(
field.value || 1
)}
onValueChange={(
value
) =>
field.onChange(
parseInt(
value,
10
)
)
}
>
<SelectTrigger>
<SelectValue placeholder="Select version" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">
{t("version1")}
</SelectItem>
<SelectItem value="2">
{t("version2")}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription>
{t("versionDescription")}
</FormDescription>
</FormItem>
)}
/>
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>{t("warning")}:</strong>{" "}
{t("proxyProtocolWarning")}
</AlertDescription>
</Alert>
</>
)}
</form>
</Form>
</SettingsSectionForm>
<form action={formAction} className="flex justify-end">
<Button
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
>
{t("saveProxyProtocol")}
</Button>
</form>
</SettingsSectionBody>
</SettingsSection>
);
}

View File

@@ -0,0 +1,43 @@
"use client";
import {
SettingsContainer,
} from "@app/components/Settings";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { resourceQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query";
import {
ProxyResourceTargetsForm
} from "@app/app/[orgId]/settings/resources/public/ProxyResourceTargetsForm";
import {
use,
} from "react";
export default function ReverseProxyTargetsPage(props: {
params: Promise<{ resourceId: number; orgId: string }>;
}) {
const params = use(props.params);
const { resource, updateResource } = useResourceContext();
const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery(
resourceQueries.resourceTargets({
resourceId: resource.resourceId
})
);
if (isLoadingTargets) {
return null;
}
return (
<SettingsContainer>
<ProxyResourceTargetsForm
orgId={params.orgId}
isHttp={["http", "ssh", "rdp", "vnc"].includes(resource.mode)}
initialTargets={remoteTargets}
resource={resource}
updateResource={updateResource}
/>
</SettingsContainer>
);
}