"use client"; import { SettingsContainer, SettingsFormCell, SettingsFormGrid, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; import { StrategySelect } from "@app/components/StrategySelect"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import HeaderTitle from "@app/components/SettingsSectionTitle"; import { z } from "zod"; import { createElement, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Input } from "@app/components/ui/input"; import { ChevronDown, ChevronUp, InfoIcon, Terminal } from "lucide-react"; import { Button } from "@app/components/ui/button"; import CopyTextBox from "@app/components/CopyTextBox"; import CopyToClipboard from "@app/components/CopyToClipboard"; import { InfoSection, InfoSectionContent, InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; import { FaApple, FaCubes, FaDocker, FaFreebsd, FaWindows } from "react-icons/fa"; import { SiNixos, SiKubernetes } from "react-icons/si"; import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { generateKeypair } from "../[niceId]/wireguardConfig"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { generateWireGuardConfig } from "@app/lib/wireguard"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { CreateSiteBody, CreateSiteResponse, PickSiteDefaultsResponse } from "@server/routers/site"; import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; import { toast } from "@app/hooks/useToast"; import { AxiosResponse } from "axios"; import { useParams, useRouter } from "next/navigation"; import { QRCodeCanvas } from "qrcode.react"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { NewtSiteInstallCommands } from "@app/components/newt-install-commands"; type SiteType = "newt" | "wireguard" | "local"; interface TunnelTypeOption { id: SiteType; title: string; description: string; disabled?: boolean; } interface RemoteExitNodeOption { id: string; title: string; description: string; disabled?: boolean; } type CommandItem = string | { title: string; command: string }; type Commands = { unix: Record; windows: Record; docker: Record; kubernetes: Record; podman: Record; nixos: Record; }; const platforms = [ "unix", "docker", "kubernetes", "podman", "windows", "nixos" ] as const; type Platform = (typeof platforms)[number]; export default function Page() { const { env } = useEnvContext(); const api = createApiClient({ env }); const { orgId } = useParams(); const router = useRouter(); const t = useTranslations(); const createSiteFormSchema = z .object({ name: z .string() .min(2, { message: t("nameMin", { len: 2 }) }) .max(30, { message: t("nameMax", { len: 30 }) }), method: z.enum(["newt", "wireguard", "local"]), copied: z.boolean(), clientAddress: z.string().optional(), acceptClients: z.boolean(), exitNodeId: z.number().optional() }) .refine( (data) => { if (data.method !== "local") { // return data.copied; return true; } // For local sites, require exitNodeId return build == "saas" ? data.exitNodeId !== undefined : true; }, { message: t("sitesConfirmCopy"), path: ["copied"] } ) .refine( (data) => { if (data.method === "local" && build == "saas") { return data.exitNodeId !== undefined; } return true; }, { message: t("remoteExitNodeRequired"), path: ["exitNodeId"] } ); type CreateSiteFormValues = z.infer; const [tunnelTypes, setTunnelTypes] = useState< ReadonlyArray >([ { id: "newt", title: t("siteNewtTunnel"), description: t("siteNewtTunnelDescription"), disabled: true }, ...(env.flags.disableBasicWireguardSites || build == "saas" ? [] : [ { id: "wireguard" as SiteType, title: t("siteWg"), description: t("siteWgDescription"), disabled: true } ]), ...(env.flags.disableLocalSites ? [] : [ { id: "local" as SiteType, title: t("local"), description: build == "saas" ? t("siteLocalDescriptionSaas") : t("siteLocalDescription") } ]) ]); const [loadingPage, setLoadingPage] = useState(true); const [newtId, setNewtId] = useState(""); const [newtSecret, setNewtSecret] = useState(""); const [newtEndpoint, setNewtEndpoint] = useState(""); const [clientAddress, setClientAddress] = useState(""); const [publicKey, setPublicKey] = useState(""); const [privateKey, setPrivateKey] = useState(""); const [wgConfig, setWgConfig] = useState(""); const [createLoading, setCreateLoading] = useState(false); const [newtVersion, setNewtVersion] = useState("latest"); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [siteDefaults, setSiteDefaults] = useState(null); const [remoteExitNodeOptions, setRemoteExitNodeOptions] = useState< ReadonlyArray >([]); const [selectedExitNodeId, setSelectedExitNodeId] = useState< string | undefined >(); const form = useForm({ resolver: zodResolver(createSiteFormSchema), defaultValues: { name: "", copied: false, method: "newt", clientAddress: "", acceptClients: true, exitNodeId: undefined } }); async function onSubmit(data: CreateSiteFormValues) { setCreateLoading(true); let payload: CreateSiteBody = { name: data.name, type: data.method }; if (data.method == "wireguard") { if (!siteDefaults || !wgConfig) { toast({ variant: "destructive", title: t("siteErrorCreate"), description: t("siteErrorCreateKeyPair") }); setCreateLoading(false); return; } payload = { ...payload, subnet: siteDefaults.subnet, exitNodeId: siteDefaults.exitNodeId, pubKey: publicKey }; } if (data.method === "newt") { if (!siteDefaults) { toast({ variant: "destructive", title: t("siteErrorCreate"), description: t("siteErrorCreateDefaults") }); setCreateLoading(false); return; } payload = { ...payload, subnet: siteDefaults.subnet, exitNodeId: siteDefaults.exitNodeId, secret: siteDefaults.newtSecret, newtId: siteDefaults.newtId, address: clientAddress }; } if (data.method === "local" && build == "saas") { if (!data.exitNodeId) { toast({ variant: "destructive", title: t("siteErrorCreate"), description: t("remoteExitNodeRequired") }); setCreateLoading(false); return; } payload = { ...payload, exitNodeId: data.exitNodeId }; } const res = await api .put< AxiosResponse >(`/org/${orgId}/site/`, payload) .catch((e) => { toast({ variant: "destructive", title: t("siteErrorCreate"), description: formatAxiosError(e) }); }); if (res && res.status === 201) { const data = res.data.data; router.push(`/${orgId}/settings/sites/${data.niceId}`); } setCreateLoading(false); } useEffect(() => { const load = async () => { setLoadingPage(true); let currentNewtVersion = "latest"; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 3000); const response = await fetch( `https://api.github.com/repos/fosrl/newt/releases/latest`, { signal: controller.signal } ); clearTimeout(timeoutId); if (!response.ok) { throw new Error( t("newtErrorFetchReleases", { err: response.statusText }) ); } const data = await response.json(); const latestVersion = data.tag_name; currentNewtVersion = latestVersion; setNewtVersion(latestVersion); } catch (error) { if (error instanceof Error && error.name === "AbortError") { console.error(t("newtErrorFetchTimeout")); } else { console.error( t("newtErrorFetchLatest", { err: error instanceof Error ? error.message : String(error) }) ); } } const generatedKeypair = generateKeypair(); const privateKey = generatedKeypair.privateKey; const publicKey = generatedKeypair.publicKey; setPrivateKey(privateKey); setPublicKey(publicKey); await api .get(`/org/${orgId}/pick-site-defaults`) .catch((e) => { // update the default value of the form to be local method only if local sites are not disabled if (!env.flags.disableLocalSites) { form.setValue("method", "local"); } }) .then((res) => { if (res && res.status === 200) { const data = res.data.data; setSiteDefaults(data); const newtId = data.newtId; const newtSecret = data.newtSecret; const newtEndpoint = data.endpoint; const clientAddress = data.clientAddress; setNewtId(newtId); setNewtSecret(newtSecret); setNewtEndpoint(newtEndpoint); setClientAddress(clientAddress); const wgConfig = generateWireGuardConfig( privateKey, data.publicKey, data.subnet, data.address, data.endpoint, data.listenPort ); setWgConfig(wgConfig); setTunnelTypes((prev: any) => { return prev.map((item: any) => { return { ...item, disabled: false }; }); }); } }); if (build === "saas") { // Fetch remote exit nodes for local sites try { const remoteExitNodesRes = await api.get< AxiosResponse >(`/org/${orgId}/remote-exit-nodes`); if ( remoteExitNodesRes && remoteExitNodesRes.status === 200 ) { const exitNodes = remoteExitNodesRes.data.data.remoteExitNodes; // Convert to options for StrategySelect const exitNodeOptions: RemoteExitNodeOption[] = exitNodes .filter((node) => node.exitNodeId !== null) .map((node) => ({ id: node.exitNodeId!.toString(), title: node.name, description: `${node.address?.split("/")[0] || "N/A"} - ${node.endpoint || "N/A"}` })); setRemoteExitNodeOptions(exitNodeOptions); if (exitNodeOptions.length === 0) { // No remote exit nodes available - remove local option and default to newt setTunnelTypes((prev: any) => prev.filter((item: any) => item.id !== "local") ); form.setValue("method", "newt"); } } } catch (error) { console.error("Failed to fetch remote exit nodes:", error); // If fetch fails, no remote exit nodes available - remove local option and default to newt setTunnelTypes((prev: any) => prev.filter((item: any) => item.id !== "local") ); form.setValue("method", "newt"); } } setLoadingPage(false); }; load(); }, []); // Sync form exitNodeId value with local state useEffect(() => { if (build !== "saas") { // dont update the form return; } form.setValue( "exitNodeId", selectedExitNodeId ? parseInt(selectedExitNodeId) : undefined ); }, [selectedExitNodeId, form]); return ( <>
{!loadingPage && (
{t("siteInfo")} {tunnelTypes.length > 1 && ( <>
{t("type")}
{ form.setValue("method", value); }} cols={3} /> )}
{ if (e.key === "Enter") { e.preventDefault(); // block default enter refresh } }} id="create-site-form" > ( {t("name")} {t( "siteNameDescription" )} )} /> {form.watch("method") === "newt" && ( <> {showAdvancedSettings && ( ( {t( "siteAddress" )} { setClientAddress( e .target .value ); field.onChange( e .target .value ); }} /> {t( "siteAddressDescription" )} )} /> )} )}
{form.watch("method") === "newt" && ( <> {t("siteNewtCredentials")} {t( "siteNewtCredentialsDescription" )} {t("newtEndpoint")} {t("newtId")} {t("newtSecretKey")} )} {form.watch("method") === "wireguard" && ( {t("WgConfiguration")} {t("WgConfigurationDescription")}
)} {build === "saas" && form.watch("method") === "local" && ( {t("remoteExitNodeSelection")} {t( "remoteExitNodeSelectionDescription" )} {remoteExitNodeOptions.length > 0 ? ( { setSelectedExitNodeId( value ); }} cols={1} /> ) : ( {t( "noRemoteExitNodesAvailable" )} {t( "noRemoteExitNodesAvailableDescription" )} )} )}
)} ); }