mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-10 20:56:39 +00:00
Chungus
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
createRemoteExitNode?: () => void;
|
||||
onRefresh?: () => void;
|
||||
isRefreshing?: boolean;
|
||||
}
|
||||
|
||||
export function ExitNodesDataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
createRemoteExitNode,
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title={t('remoteExitNodes')}
|
||||
searchPlaceholder={t('searchRemoteExitNodes')}
|
||||
searchColumn="name"
|
||||
onAdd={createRemoteExitNode}
|
||||
addButtonText={t('remoteExitNodeAdd')}
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
defaultSort={{
|
||||
id: "name",
|
||||
desc: false
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { ExitNodesDataTable } from "./ExitNodesDataTable";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
|
||||
export type RemoteExitNodeRow = {
|
||||
id: string;
|
||||
exitNodeId: number | null;
|
||||
name: string;
|
||||
address: string;
|
||||
endpoint: string;
|
||||
orgId: string;
|
||||
type: string | null;
|
||||
online: boolean;
|
||||
dateCreated: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
type ExitNodesTableProps = {
|
||||
remoteExitNodes: RemoteExitNodeRow[];
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export default function ExitNodesTable({
|
||||
remoteExitNodes,
|
||||
orgId
|
||||
}: ExitNodesTableProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedNode, setSelectedNode] = useState<RemoteExitNodeRow | null>(
|
||||
null
|
||||
);
|
||||
const [rows, setRows] = useState<RemoteExitNodeRow[]>(remoteExitNodes);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
setRows(remoteExitNodes);
|
||||
}, [remoteExitNodes]);
|
||||
|
||||
const refreshData = async () => {
|
||||
console.log("Data refreshed");
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("refreshError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRemoteExitNode = (remoteExitNodeId: string) => {
|
||||
api.delete(`/org/${orgId}/remote-exit-node/${remoteExitNodeId}`)
|
||||
.catch((e) => {
|
||||
console.error(t("remoteExitNodeErrorDelete"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("remoteExitNodeErrorDelete"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("remoteExitNodeErrorDelete")
|
||||
)
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
|
||||
const newRows = rows.filter(
|
||||
(row) => row.id !== remoteExitNodeId
|
||||
);
|
||||
setRows(newRows);
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnDef<RemoteExitNodeRow>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("name")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "online",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("online")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
if (originalRow.online) {
|
||||
return (
|
||||
<span 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>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span 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>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("connectionType")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
{originalRow.type === "remoteExitNode"
|
||||
? "Remote Exit Node"
|
||||
: originalRow.type}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "address",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Address
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "endpoint",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Endpoint
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "version",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Version
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
return originalRow.version || "-";
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const nodeRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedNode(nodeRow);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
{t("delete")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedNode && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsDeleteModalOpen(val);
|
||||
setSelectedNode(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
{t("remoteExitNodeQuestionRemove", {
|
||||
selectedNode:
|
||||
selectedNode?.name || selectedNode?.id
|
||||
})}
|
||||
</p>
|
||||
|
||||
<p>{t("remoteExitNodeMessageRemove")}</p>
|
||||
|
||||
<p>{t("remoteExitNodeMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("remoteExitNodeConfirmDelete")}
|
||||
onConfirm={async () =>
|
||||
deleteRemoteExitNode(selectedNode!.id)
|
||||
}
|
||||
string={selectedNode.name}
|
||||
title={t("remoteExitNodeDelete")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExitNodesDataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
createRemoteExitNode={() =>
|
||||
router.push(`/${orgId}/settings/remote-exit-nodes/create`)
|
||||
}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
export default function GeneralPage() {
|
||||
return <></>;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { internal } from "@app/lib/api";
|
||||
import { GetRemoteExitNodeResponse } from "@server/routers/private/remoteExitNode";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import RemoteExitNodeProvider from "@app/providers/PrivateRemoteExitNodeProvider";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ remoteExitNodeId: string; orgId: string }>;
|
||||
}
|
||||
|
||||
export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
const params = await props.params;
|
||||
const { children } = props;
|
||||
|
||||
let remoteExitNode = null;
|
||||
try {
|
||||
const res = await internal.get<
|
||||
AxiosResponse<GetRemoteExitNodeResponse>
|
||||
>(
|
||||
`/org/${params.orgId}/remote-exit-node/${params.remoteExitNodeId}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
remoteExitNode = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${params.orgId}/settings/remote-exit-nodes`);
|
||||
}
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={`Remote Exit Node ${remoteExitNode?.name || "Unknown"}`}
|
||||
description="Manage your remote exit node settings and configuration"
|
||||
/>
|
||||
|
||||
<RemoteExitNodeProvider remoteExitNode={remoteExitNode}>
|
||||
<div className="space-y-6">{children}</div>
|
||||
</RemoteExitNodeProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function RemoteExitNodePage(props: {
|
||||
params: Promise<{ orgId: string; remoteExitNodeId: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
redirect(
|
||||
`/${params.orgId}/settings/remote-exit-nodes/${params.remoteExitNodeId}/general`
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { z } from "zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import {
|
||||
QuickStartRemoteExitNodeResponse,
|
||||
PickRemoteExitNodeDefaultsResponse
|
||||
} from "@server/routers/private/remoteExitNode";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||
import { StrategySelect } from "@app/components/StrategySelect";
|
||||
|
||||
export default function CreateRemoteExitNodePage() {
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const { orgId } = useParams();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const t = useTranslations();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [defaults, setDefaults] =
|
||||
useState<PickRemoteExitNodeDefaultsResponse | null>(null);
|
||||
const [createdNode, setCreatedNode] =
|
||||
useState<QuickStartRemoteExitNodeResponse | null>(null);
|
||||
const [strategy, setStrategy] = useState<"adopt" | "generate">("adopt");
|
||||
|
||||
const createRemoteExitNodeFormSchema = z
|
||||
.object({
|
||||
remoteExitNodeId: z.string().optional(),
|
||||
secret: z.string().optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (strategy === "adopt") {
|
||||
return data.remoteExitNodeId && data.secret;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: t("remoteExitNodeCreate.validation.adoptRequired"),
|
||||
path: ["remoteExitNodeId"]
|
||||
}
|
||||
);
|
||||
|
||||
type CreateRemoteExitNodeFormValues = z.infer<
|
||||
typeof createRemoteExitNodeFormSchema
|
||||
>;
|
||||
|
||||
const form = useForm<CreateRemoteExitNodeFormValues>({
|
||||
resolver: zodResolver(createRemoteExitNodeFormSchema),
|
||||
defaultValues: {}
|
||||
});
|
||||
|
||||
// Check for query parameters and prefill form
|
||||
useEffect(() => {
|
||||
const remoteExitNodeId = searchParams.get("remoteExitNodeId");
|
||||
const remoteExitNodeSecret = searchParams.get("remoteExitNodeSecret");
|
||||
|
||||
if (remoteExitNodeId && remoteExitNodeSecret) {
|
||||
setStrategy("adopt");
|
||||
form.setValue("remoteExitNodeId", remoteExitNodeId);
|
||||
form.setValue("secret", remoteExitNodeSecret);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadDefaults = async () => {
|
||||
try {
|
||||
const response = await api.get<
|
||||
AxiosResponse<PickRemoteExitNodeDefaultsResponse>
|
||||
>(`/org/${orgId}/pick-remote-exit-node-defaults`);
|
||||
setDefaults(response.data.data);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t(
|
||||
"remoteExitNodeCreate.errors.loadDefaultsFailed"
|
||||
),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Only load defaults when strategy is "generate"
|
||||
if (strategy === "generate") {
|
||||
loadDefaults();
|
||||
}
|
||||
}, [strategy]);
|
||||
|
||||
const onSubmit = async (data: CreateRemoteExitNodeFormValues) => {
|
||||
if (strategy === "generate" && !defaults) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("remoteExitNodeCreate.errors.defaultsNotLoaded"),
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (strategy === "adopt" && (!data.remoteExitNodeId || !data.secret)) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("remoteExitNodeCreate.validation.adoptRequired"),
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await api.put<
|
||||
AxiosResponse<QuickStartRemoteExitNodeResponse>
|
||||
>(`/org/${orgId}/remote-exit-node`, {
|
||||
remoteExitNodeId:
|
||||
strategy === "generate"
|
||||
? defaults!.remoteExitNodeId
|
||||
: data.remoteExitNodeId!,
|
||||
secret:
|
||||
strategy === "generate" ? defaults!.secret : data.secret!
|
||||
});
|
||||
setCreatedNode(response.data.data);
|
||||
|
||||
router.push(`/${orgId}/settings/remote-exit-nodes`);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: formatAxiosError(
|
||||
error,
|
||||
t("remoteExitNodeCreate.errors.createFailed")
|
||||
),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<HeaderTitle
|
||||
title={t("remoteExitNodeCreate.title")}
|
||||
description={t("remoteExitNodeCreate.description")}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
router.push(`/${orgId}/settings/remote-exit-nodes`);
|
||||
}}
|
||||
>
|
||||
{t("remoteExitNodeCreate.viewAllButton")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("remoteExitNodeCreate.strategy.title")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("remoteExitNodeCreate.strategy.description")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<StrategySelect
|
||||
options={[
|
||||
{
|
||||
id: "adopt",
|
||||
title: t(
|
||||
"remoteExitNodeCreate.strategy.adopt.title"
|
||||
),
|
||||
description: t(
|
||||
"remoteExitNodeCreate.strategy.adopt.description"
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "generate",
|
||||
title: t(
|
||||
"remoteExitNodeCreate.strategy.generate.title"
|
||||
),
|
||||
description: t(
|
||||
"remoteExitNodeCreate.strategy.generate.description"
|
||||
)
|
||||
}
|
||||
]}
|
||||
defaultValue={strategy}
|
||||
onChange={(value) => {
|
||||
setStrategy(value);
|
||||
// Clear adopt fields when switching to generate
|
||||
if (value === "generate") {
|
||||
form.setValue("remoteExitNodeId", "");
|
||||
form.setValue("secret", "");
|
||||
}
|
||||
}}
|
||||
cols={2}
|
||||
/>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
{strategy === "adopt" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("remoteExitNodeCreate.adopt.title")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t(
|
||||
"remoteExitNodeCreate.adopt.description"
|
||||
)}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remoteExitNodeId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"remoteExitNodeCreate.adopt.nodeIdLabel"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"remoteExitNodeCreate.adopt.nodeIdDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="secret"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"remoteExitNodeCreate.adopt.secretLabel"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"remoteExitNodeCreate.adopt.secretDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{strategy === "generate" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("remoteExitNodeCreate.generate.title")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t(
|
||||
"remoteExitNodeCreate.generate.description"
|
||||
)}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<CopyTextBox
|
||||
text={`managed:
|
||||
id: "${defaults?.remoteExitNodeId}"
|
||||
secret: "${defaults?.secret}"`}
|
||||
/>
|
||||
<Alert variant="neutral" className="mt-4">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t(
|
||||
"remoteExitNodeCreate.generate.saveCredentialsTitle"
|
||||
)}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t(
|
||||
"remoteExitNodeCreate.generate.saveCredentialsDescription"
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-8">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
router.push(`/${orgId}/settings/remote-exit-nodes`);
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
>
|
||||
{strategy === "adopt"
|
||||
? t("remoteExitNodeCreate.adopt.submitButton")
|
||||
: t("remoteExitNodeCreate.generate.submitButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { ListRemoteExitNodesResponse } from "@server/routers/private/remoteExitNode";
|
||||
import { AxiosResponse } from "axios";
|
||||
import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type RemoteExitNodesPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function RemoteExitNodesPage(
|
||||
props: RemoteExitNodesPageProps
|
||||
) {
|
||||
const params = await props.params;
|
||||
let remoteExitNodes: ListRemoteExitNodesResponse["remoteExitNodes"] = [];
|
||||
try {
|
||||
const res = await internal.get<
|
||||
AxiosResponse<ListRemoteExitNodesResponse>
|
||||
>(`/org/${params.orgId}/remote-exit-nodes`, await authCookieHeader());
|
||||
remoteExitNodes = res.data.data.remoteExitNodes;
|
||||
} catch (e) {}
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
const remoteExitNodeRows: RemoteExitNodeRow[] = remoteExitNodes.map(
|
||||
(node) => {
|
||||
return {
|
||||
name: node.name,
|
||||
id: node.remoteExitNodeId,
|
||||
exitNodeId: node.exitNodeId,
|
||||
address: node.address?.split("/")[0] || "-",
|
||||
endpoint: node.endpoint || "-",
|
||||
online: node.online,
|
||||
type: node.type,
|
||||
dateCreated: node.dateCreated,
|
||||
version: node.version || undefined,
|
||||
orgId: params.orgId
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t("remoteExitNodeManageRemoteExitNodes")}
|
||||
description={t("remoteExitNodeDescription")}
|
||||
/>
|
||||
|
||||
<ExitNodesTable
|
||||
remoteExitNodes={remoteExitNodeRows}
|
||||
orgId={params.orgId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user