remote node changes

This commit is contained in:
miloschwartz
2025-12-17 16:50:39 -05:00
parent a5b203af27
commit 6f50fb8a4f
12 changed files with 103 additions and 45 deletions

View File

@@ -100,6 +100,7 @@
"siteTunnelDescription": "Determine how you want to connect to the site",
"siteNewtCredentials": "Credentials",
"siteNewtCredentialsDescription": "This is how the site will authenticate with the server",
"remoteNodeCredentialsDescription": "This is how the remote node will authenticate with the server",
"siteCredentialsSave": "Save the Credentials",
"siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
"siteInfo": "Site Information",
@@ -1671,7 +1672,7 @@
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend network connectivity and reduce reliance on the cloud",
"remoteExitNodeDescription": "Self-host your own remote relay and proxy server nodes",
"remoteExitNodes": "Nodes",
"searchRemoteExitNodes": "Search nodes...",
"remoteExitNodeAdd": "Add Node",
@@ -1681,20 +1682,22 @@
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
"remoteExitNodeDelete": "Delete Node",
"sidebarRemoteExitNodes": "Remote Nodes",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "Secret",
"remoteExitNodeCreate": {
"title": "Create Node",
"description": "Create a new node to extend network connectivity",
"title": "Create Remote Node",
"description": "Create a new self-hosted remote relay and proxy server node",
"viewAllButton": "View All Nodes",
"strategy": {
"title": "Creation Strategy",
"description": "Choose this to manually configure the node or generate new credentials.",
"description": "Select how you want to create the remote node",
"adopt": {
"title": "Adopt Node",
"description": "Choose this if you already have the credentials for the node."
},
"generate": {
"title": "Generate Keys",
"description": "Choose this if you want to generate new keys for the node"
"description": "Choose this if you want to generate new keys for the node."
}
},
"adopt": {
@@ -2308,5 +2311,6 @@
"organizationLoginPageTitle": "Organization Login Page",
"organizationLoginPageDescription": "Customize the login page for this organization",
"resourceLoginPageTitle": "Resource Login Page",
"resourceLoginPageDescription": "Customize the login page for individual resources"
"resourceLoginPageDescription": "Customize the login page for individual resources",
"enterConfirmation": "Enter confirmation"
}

View File

@@ -1,5 +1,6 @@
import { db, exitNodes, newts } from "@server/db";
import { orgs, roleSites, sites, userSites } from "@server/db";
import { remoteExitNodes } from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
@@ -104,12 +105,17 @@ function querySites(orgId: string, accessibleSiteIds: number[]) {
newtVersion: newts.version,
exitNodeId: sites.exitNodeId,
exitNodeName: exitNodes.name,
exitNodeEndpoint: exitNodes.endpoint
exitNodeEndpoint: exitNodes.endpoint,
remoteExitNodeId: remoteExitNodes.remoteExitNodeId
})
.from(sites)
.leftJoin(orgs, eq(sites.orgId, orgs.orgId))
.leftJoin(newts, eq(newts.siteId, sites.siteId))
.leftJoin(exitNodes, eq(exitNodes.exitNodeId, sites.exitNodeId))
.leftJoin(
remoteExitNodes,
eq(remoteExitNodes.exitNodeId, sites.exitNodeId)
)
.where(
and(
inArray(sites.siteId, accessibleSiteIds),

View File

@@ -131,10 +131,10 @@ export default function CredentialsPage() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("generatedcredentials")}
{t("credentials")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("regenerateCredentials")}
{t("remoteNodeCredentialsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -143,7 +143,7 @@ export default function CredentialsPage() {
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
{t("endpoint") || "Endpoint"}
{t("endpoint")}
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
@@ -153,8 +153,7 @@ export default function CredentialsPage() {
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("remoteExitNodeId") ||
"Remote Exit Node ID"}
{t("remoteExitNodeId")}
</InfoSectionTitle>
<InfoSectionContent>
{displayRemoteExitNodeId ? (
@@ -168,7 +167,7 @@ export default function CredentialsPage() {
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("secretKey") || "Secret Key"}
{t("remoteExitNodeSecretKey")}
</InfoSectionTitle>
<InfoSectionContent>
{displaySecret ? (

View File

@@ -43,7 +43,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
return (
<>
<SettingsSectionTitle
title={`Remote Exit Node ${remoteExitNode?.name || "Unknown"}`}
title={`Remote Node ${remoteExitNode?.name || "Unknown"}`}
description="Manage your remote exit node settings and configuration"
/>

View File

@@ -2,7 +2,9 @@ import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
import { AxiosResponse } from "axios";
import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable";
import ExitNodesTable, {
RemoteExitNodeRow
} from "@app/components/ExitNodesTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { getTranslations } from "next-intl/server";

View File

@@ -53,7 +53,8 @@ export default async function SitesPage(props: SitesPageProps) {
newtVersion: site.newtVersion || undefined,
newtUpdateAvailable: site.newtUpdateAvailable || false,
exitNodeName: site.exitNodeName || undefined,
exitNodeEndpoint: site.exitNodeEndpoint || undefined
exitNodeEndpoint: site.exitNodeEndpoint || undefined,
remoteExitNodeId: (site as any).remoteExitNodeId || undefined
};
});

View File

@@ -116,7 +116,12 @@ export default function ConfirmDeleteDialog({
render={({ field }) => (
<FormItem>
<FormControl>
<Input {...field} />
<Input
{...field}
placeholder={t(
"enterConfirmation"
)}
/>
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -19,7 +19,7 @@ export default function ExitNodeInfoCard({}: ExitNodeInfoCardProps) {
return (
<Alert>
<AlertDescription className="mt-4">
<AlertDescription>
<InfoSections cols={2}>
<>
<InfoSection>

View File

@@ -10,6 +10,8 @@ interface DataTableProps<TData, TValue> {
createRemoteExitNode?: () => void;
onRefresh?: () => void;
isRefreshing?: boolean;
columnVisibility?: Record<string, boolean>;
enableColumnVisibility?: boolean;
}
export function ExitNodesDataTable<TData, TValue>({
@@ -17,7 +19,9 @@ export function ExitNodesDataTable<TData, TValue>({
data,
createRemoteExitNode,
onRefresh,
isRefreshing
isRefreshing,
columnVisibility,
enableColumnVisibility
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
@@ -36,6 +40,10 @@ export function ExitNodesDataTable<TData, TValue>({
id: "name",
desc: false
}}
columnVisibility={columnVisibility}
enableColumnVisibility={enableColumnVisibility}
stickyLeftColumn="name"
stickyRightColumn="actions"
/>
);
}

View File

@@ -2,7 +2,7 @@
import { ColumnDef } from "@tanstack/react-table";
import { ExtendedColumnDef } from "@app/components/ui/data-table";
import { ExitNodesDataTable } from "./ExitNodesDataTable";
import { ExitNodesDataTable } from "@app/components/ExitNodesDataTable";
import {
DropdownMenu,
DropdownMenuContent,
@@ -246,12 +246,13 @@ export default function ExitNodesTable({
},
{
id: "actions",
header: () => <span className="p-3">{t("actions")}</span>,
enableHiding: false,
header: () => <span className="p-3"></span>,
cell: ({ row }) => {
const nodeRow = row.original;
const remoteExitNodeId = nodeRow.id;
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
@@ -283,7 +284,7 @@ export default function ExitNodesTable({
<Link
href={`/${nodeRow.orgId}/settings/remote-exit-nodes/${remoteExitNodeId}`}
>
<Button variant={"secondary"} size="sm">
<Button variant={"outline"}>
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
@@ -327,6 +328,11 @@ export default function ExitNodesTable({
}
onRefresh={refreshData}
isRefreshing={isRefreshing}
columnVisibility={{
type: false,
address: false,
}}
enableColumnVisibility={true}
/>
</>
);

View File

@@ -39,7 +39,8 @@ export function HorizontalTabs({
.replace("{niceId}", params.niceId as string)
.replace("{userId}", params.userId as string)
.replace("{clientId}", params.clientId as string)
.replace("{apiKeyId}", params.apiKeyId as string);
.replace("{apiKeyId}", params.apiKeyId as string)
.replace("{remoteExitNodeId}", params.remoteExitNodeId as string);
}
return (

View File

@@ -13,6 +13,7 @@ import { Button } from "@app/components/ui/button";
import {
ArrowRight,
ArrowUpDown,
ArrowUpRight,
Check,
MoreHorizontal,
X
@@ -46,6 +47,7 @@ export type SiteRow = {
address?: string;
exitNodeName?: string;
exitNodeEndpoint?: string;
remoteExitNodeId?: string;
};
type SitesTableProps = {
@@ -303,27 +305,51 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
},
cell: ({ row }) => {
const originalRow = row.original;
return originalRow.exitNodeName ? (
<div className="flex items-center space-x-2">
<span>{originalRow.exitNodeName}</span>
{build == "saas" &&
originalRow.exitNodeName &&
[
"mercury",
"venus",
"earth",
"mars",
"jupiter",
"saturn",
"uranus",
"neptune"
].includes(
originalRow.exitNodeName.toLowerCase()
) && <Badge variant="secondary">Cloud</Badge>}
</div>
) : (
"-"
);
if (!originalRow.exitNodeName) {
return "-";
}
const isCloudNode =
build == "saas" &&
originalRow.exitNodeName &&
[
"mercury",
"venus",
"earth",
"mars",
"jupiter",
"saturn",
"uranus",
"neptune"
].includes(originalRow.exitNodeName.toLowerCase());
if (isCloudNode) {
const capitalizedName =
originalRow.exitNodeName.charAt(0).toUpperCase() +
originalRow.exitNodeName.slice(1).toLowerCase();
return (
<Badge variant="secondary">
Pangolin {capitalizedName}
</Badge>
);
}
// Self-hosted node
if (originalRow.remoteExitNodeId) {
return (
<Link
href={`/${originalRow.orgId}/settings/remote-exit-nodes/${originalRow.remoteExitNodeId}`}
>
<Button variant="outline">
{originalRow.exitNodeName}
<ArrowUpRight className="ml-2 h-4 w-4" />
</Button>
</Link>
);
}
// Fallback if no remoteExitNodeId
return <span>{originalRow.exitNodeName}</span>;
}
},
{