Option to regenerate remote-nodes keys

This commit is contained in:
Pallavi Kumari
2025-10-24 23:36:20 +05:30
parent 2a7529c39e
commit 9f9aa07c2d
9 changed files with 368 additions and 3 deletions

View File

@@ -232,6 +232,7 @@ export default function ExitNodesTable({
id: "actions",
cell: ({ row }) => {
const nodeRow = row.original;
const remoteExitNodeId = nodeRow.id;
return (
<div className="flex items-center justify-end gap-2">
<DropdownMenu>
@@ -242,6 +243,14 @@ export default function ExitNodesTable({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/${nodeRow.orgId}/settings/remote-exit-nodes/${remoteExitNodeId}`}
>
<DropdownMenuItem>
{t("viewSettings")}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedNode(nodeRow);

View File

@@ -1,3 +1,171 @@
"use client";
import { useEffect, useState } from "react";
import {
SettingsContainer,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import { Button } from "@app/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon } from "lucide-react";
import CopyTextBox from "@app/components/CopyTextBox";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { useParams, useRouter } from "next/navigation";
import { AxiosResponse } from "axios";
import { useTranslations } from "next-intl";
import {
PickRemoteExitNodeDefaultsResponse,
QuickStartRemoteExitNodeResponse
} from "@server/routers/remoteExitNode/types";
import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext";
export default function GeneralPage() {
return <></>;
const { env } = useEnvContext();
const api = createApiClient({ env });
const { orgId } = useParams();
const router = useRouter();
const t = useTranslations();
const { remoteExitNode, updateRemoteExitNode } = useRemoteExitNodeContext();
const [credentials, setCredentials] =
useState<PickRemoteExitNodeDefaultsResponse | null>(null);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
// Clear credentials when user leaves/reloads
useEffect(() => {
const clearCreds = () => setCredentials(null);
window.addEventListener("beforeunload", clearCreds);
return () => window.removeEventListener("beforeunload", clearCreds);
}, []);
const handleRegenerate = async () => {
try {
setLoading(true);
const response = await api.get<
AxiosResponse<PickRemoteExitNodeDefaultsResponse>
>(`/org/${orgId}/pick-remote-exit-node-defaults`);
setCredentials(response.data.data);
toast({
title: t("success"),
description: t("Credentials generated successfully."),
});
} catch (error) {
toast({
title: t("error"),
description: formatAxiosError(
error,
t("Failed to generate credentials")
),
variant: "destructive",
});
} finally {
setLoading(false);
}
};
const handleSave = async () => {
if (!credentials) return;
try {
setSaving(true);
const response = await api.put<
AxiosResponse<QuickStartRemoteExitNodeResponse>
>(`/org/${orgId}/update-remote-exit-node`, {
remoteExitNodeId: remoteExitNode.remoteExitNodeId,
secret: credentials.secret,
});
toast({
title: t("success"),
description: t("Credentials saved successfully."),
});
// For security, clear them from UI
setCredentials(null);
} catch (error) {
toast({
title: t("error"),
description: formatAxiosError(
error,
t("Failed to save credentials")
),
variant: "destructive",
});
} finally {
setSaving(false);
}
};
return (
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("Generated Credentials")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("Regenerate and save your managed credentials")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{!credentials ? (
<Button
onClick={handleRegenerate}
loading={loading}
disabled={loading}
>
{t("Regenerate Credentials")}
</Button>
) : (
<>
<CopyTextBox
text={`managed:
id: "${remoteExitNode.remoteExitNodeId}"
secret: "${credentials.secret}"`}
/>
<Alert variant="neutral" className="mt-4">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
{t("Copy and save these credentials")}
</AlertTitle>
<AlertDescription>
{t(
"These credentials will not be shown again after you leave this page. Save them securely now."
)}
</AlertDescription>
</Alert>
<div className="flex justify-end mt-6 space-x-2">
<Button
variant="outline"
onClick={() => setCredentials(null)}
>
{t("Cancel")}
</Button>
<Button
onClick={handleSave}
loading={saving}
disabled={saving}
>
{t("Save Credentials")}
</Button>
</div>
</>
)}
</SettingsSectionBody>
</SettingsSection>
</SettingsContainer>
);
}

View File

@@ -6,6 +6,8 @@ import { authCookieHeader } from "@app/lib/api/cookies";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { getTranslations } from "next-intl/server";
import RemoteExitNodeProvider from "@app/providers/RemoteExitNodeProvider";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import ExitNodeInfoCard from "@app/components/ExitNodeInfoCard";
interface SettingsLayoutProps {
children: React.ReactNode;
@@ -31,6 +33,13 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const t = await getTranslations();
const navItems = [
{
title: t('general'),
href: "/{orgId}/settings/remote-exit-nodes/{remoteExitNodeId}/general"
}
];
return (
<>
<SettingsSectionTitle
@@ -39,7 +48,10 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
/>
<RemoteExitNodeProvider remoteExitNode={remoteExitNode}>
<div className="space-y-6">{children}</div>
<div className="space-y-6">
<ExitNodeInfoCard />
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
</div>
</RemoteExitNodeProvider>
</>
);

View File

@@ -0,0 +1,52 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { InfoIcon } from "lucide-react";
import {
InfoSection,
InfoSectionContent,
InfoSections,
InfoSectionTitle
} from "@app/components/InfoSection";
import { useTranslations } from "next-intl";
import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext";
type ExitNodeInfoCardProps = {};
export default function ExitNodeInfoCard({}: ExitNodeInfoCardProps) {
const { remoteExitNode, updateRemoteExitNode } = useRemoteExitNodeContext();
const t = useTranslations();
return (
<Alert>
<AlertDescription className="mt-4">
<InfoSections cols={2}>
<>
<InfoSection>
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
<InfoSectionContent>
{remoteExitNode.online ? (
<div className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>{t("online")}</span>
</div>
) : (
<div className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>{t("offline")}</span>
</div>
)}
</InfoSectionContent>
</InfoSection>
</>
<InfoSection>
<InfoSectionTitle>{t("address")}</InfoSectionTitle>
<InfoSectionContent>
{remoteExitNode.address}
</InfoSectionContent>
</InfoSection>
</InfoSections>
</AlertDescription>
</Alert>
);
}

View File

@@ -2,11 +2,15 @@
import RemoteExitNodeContext from "@app/contexts/remoteExitNodeContext";
import { build } from "@server/build";
import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
import { useContext } from "react";
export function useRemoteExitNodeContext() {
if (build == "oss") {
return null;
return {
remoteExitNode: {} as GetRemoteExitNodeResponse,
updateRemoteExitNode: () => {},
};
}
const context = useContext(RemoteExitNodeContext);
if (context === undefined) {