diff --git a/messages/en-US.json b/messages/en-US.json
index bf038b031..937032c18 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -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"
}
diff --git a/server/setup/scriptsPg/1.19.0.ts b/server/setup/scriptsPg/1.19.0.ts
index 4e5b67faa..68aa83afc 100644
--- a/server/setup/scriptsPg/1.19.0.ts
+++ b/server/setup/scriptsPg/1.19.0.ts
@@ -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}
diff --git a/server/setup/scriptsSqlite/1.19.0.ts b/server/setup/scriptsSqlite/1.19.0.ts
index 0a08b5abd..7f0e726d1 100644
--- a/server/setup/scriptsSqlite/1.19.0.ts
+++ b/server/setup/scriptsSqlite/1.19.0.ts
@@ -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);
diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx
index c65ddb9b2..ba6e24699 100644
--- a/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx
+++ b/src/app/[orgId]/settings/resources/public/[niceId]/http/page.tsx
@@ -101,12 +101,6 @@ export default function ReverseProxyTargetsPage(props: {
/>
)}
- {resource.mode == "tcp" && (
-
- )}
);
}
@@ -405,205 +399,4 @@ function ProxyResourceHttpForm({
);
-}
-
-function ProxyResourceProtocolForm({
- resource,
- updateResource
-}: Pick) {
- 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 (
-
-
-
- {t("proxyProtocol")}
-
-
- {t("proxyProtocolDescription")}
-
-
-
-
-
-
-
-
-
-
- );
-}
+}
\ No newline at end of file
diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/tcp/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/tcp/page.tsx
new file mode 100644
index 000000000..0e14ba9aa
--- /dev/null
+++ b/src/app/[orgId]/settings/resources/public/[niceId]/tcp/page.tsx
@@ -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 (
+
+
+
+ {resource.mode == "tcp" && (
+
+ )}
+
+ );
+}
+
+function ProxyResourceProtocolForm({
+ resource,
+ updateResource
+}: Pick) {
+ 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 (
+
+
+
+ {t("proxyProtocol")}
+
+
+ {t("proxyProtocolDescription")}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/udp/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/udp/page.tsx
new file mode 100644
index 000000000..42cb5daea
--- /dev/null
+++ b/src/app/[orgId]/settings/resources/public/[niceId]/udp/page.tsx
@@ -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 (
+
+
+
+ );
+}
\ No newline at end of file