"use client"; import { SettingsContainer, 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 { 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 { useEnvContext } from "@app/hooks/useEnvContext"; import { CreateSiteBody, CreateSiteResponse, PickSiteDefaultsResponse } from "@server/routers/site"; 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"; type SiteType = "newt" | "wireguard" | "local"; interface TunnelTypeOption { id: SiteType; title: string; description: string; disabled?: boolean; } type Commands = { mac: Record; linux: Record; freebsd: Record; windows: Record; docker: Record; kubernetes: Record; podman: Record; nixos: Record; }; const platforms = [ "linux", "docker", "kubernetes", "podman", "mac", "windows", "freebsd", "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() }) .refine( (data) => { if (data.method !== "local") { // return data.copied; return true; } return true; }, { message: t("sitesConfirmCopy"), path: ["copied"] } ); type CreateSiteFormValues = z.infer; const [tunnelTypes, setTunnelTypes] = useState< ReadonlyArray >([ { id: "newt", title: t("siteNewtTunnel"), description: t("siteNewtTunnelDescription"), disabled: true }, ...(env.flags.disableBasicWireguardSites ? [] : [ { id: "wireguard" as SiteType, title: t("siteWg"), description: build == "saas" ? t("siteWgDescriptionSaas") : 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 [platform, setPlatform] = useState("linux"); const [architecture, setArchitecture] = useState("amd64"); const [commands, setCommands] = useState(null); 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 [acceptClients, setAcceptClients] = useState(false); const [newtVersion, setNewtVersion] = useState("latest"); const [siteDefaults, setSiteDefaults] = useState(null); const hydrateWireGuardConfig = ( privateKey: string, publicKey: string, subnet: string, address: string, endpoint: string, listenPort: string ) => { const wgConfig = `[Interface] Address = ${subnet} ListenPort = 51820 PrivateKey = ${privateKey} [Peer] PublicKey = ${publicKey} AllowedIPs = ${address.split("/")[0]}/32 Endpoint = ${endpoint}:${listenPort} PersistentKeepalive = 5`; setWgConfig(wgConfig); }; const hydrateCommands = ( id: string, secret: string, endpoint: string, version: string, acceptClients: boolean = false ) => { const acceptClientsFlag = acceptClients ? " --accept-clients" : ""; const acceptClientsEnv = acceptClients ? "\n - ACCEPT_CLIENTS=true" : ""; const commands = { mac: { All: [ `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] // "Intel x64 (amd64)": [ // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` // ] }, linux: { All: [ `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] // arm64: [ // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` // ], // arm32: [ // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` // ], // arm32v6: [ // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` // ], // riscv64: [ // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` // ] }, freebsd: { All: [ `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] // arm64: [ // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` // ] }, windows: { x64: [ `curl -o newt.exe -L "https://github.com/fosrl/newt/releases/download/${version}/newt_windows_amd64.exe"`, `newt.exe --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] }, docker: { "Docker Compose": [ `services: newt: image: fosrl/newt container_name: newt restart: unless-stopped environment: - PANGOLIN_ENDPOINT=${endpoint} - NEWT_ID=${id} - NEWT_SECRET=${secret}${acceptClientsEnv}` ], "Docker Run": [ `docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] }, kubernetes: { "Helm Chart": [ `helm repo add fossorial https://charts.fossorial.io`, `helm repo update fossorial`, `helm install newt fossorial/newt \\ --create-namespace \\ --set newtInstances[0].name="main-tunnel" \\ --set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\ --set-string newtInstances[0].auth.keys.idKey="${id}" \\ --set-string newtInstances[0].auth.keys.secretKey="${secret}"` ] }, podman: { "Podman Quadlet": [ `[Unit] Description=Newt container [Container] ContainerName=newt Image=docker.io/fosrl/newt Environment=PANGOLIN_ENDPOINT=${endpoint} Environment=NEWT_ID=${id} Environment=NEWT_SECRET=${secret}${acceptClients ? "\nEnvironment=ACCEPT_CLIENTS=true" : ""} # Secret=newt-secret,type=env,target=NEWT_SECRET [Service] Restart=always [Install] WantedBy=default.target` ], "Podman Run": [ `podman run -dit docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] }, nixos: { All: [ `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ], // aarch64: [ // `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` // ] } }; setCommands(commands); }; const getArchitectures = () => { switch (platform) { case "linux": // return ["amd64", "arm64", "arm32", "arm32v6", "riscv64"]; return ["All"]; case "mac": // return ["Apple Silicon (arm64)", "Intel x64 (amd64)"]; return ["All"]; case "windows": return ["x64"]; case "docker": return ["Docker Compose", "Docker Run"]; case "kubernetes": return ["Helm Chart"]; case "podman": return ["Podman Quadlet", "Podman Run"]; case "freebsd": // return ["amd64", "arm64"]; return ["All"]; case "nixos": // return ["x86_64", "aarch64"]; return ["All"]; default: return ["x64"]; } }; const getPlatformName = (platformName: string) => { switch (platformName) { case "windows": return "Windows"; case "mac": return "macOS"; case "docker": return "Docker"; case "kubernetes": return "Kubernetes"; case "podman": return "Podman"; case "freebsd": return "FreeBSD"; case "nixos": return "NixOS"; default: return "Linux"; } }; const getCommand = () => { const placeholder = [t("unknownCommand")]; if (!commands) { return placeholder; } let platformCommands = commands[platform as keyof Commands]; if (!platformCommands) { // get first key const firstPlatform = Object.keys(commands)[0] as Platform; platformCommands = commands[firstPlatform as keyof Commands]; setPlatform(firstPlatform); } let architectureCommands = platformCommands[architecture]; if (!architectureCommands) { // get first key const firstArchitecture = Object.keys(platformCommands)[0]; architectureCommands = platformCommands[firstArchitecture]; setArchitecture(firstArchitecture); } return architectureCommands || placeholder; }; const getPlatformIcon = (platformName: string) => { switch (platformName) { case "windows": return ; case "mac": return ; case "docker": return ; case "kubernetes": return ; case "podman": return ; case "freebsd": return ; case "nixos": return ; default: return ; } }; const form = useForm({ resolver: zodResolver(createSiteFormSchema), defaultValues: { name: "", copied: false, method: "newt", clientAddress: "", acceptClients: false } }); async function onSubmit(data: CreateSiteFormValues) { setCreateLoading(true); let payload: CreateSiteBody = { name: data.name, type: data.method as "newt" | "wireguard" | "local" }; 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 }; } 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 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); hydrateCommands( newtId, newtSecret, env.app.dashboardUrl, currentNewtVersion, acceptClients ); hydrateWireGuardConfig( privateKey, data.publicKey, data.subnet, data.address, data.endpoint, data.listenPort ); setTunnelTypes((prev: any) => { return prev.map((item: any) => { return { ...item, disabled: false }; }); }); } }); setLoadingPage(false); }; load(); }, []); // Sync form acceptClients value with local state useEffect(() => { form.setValue("acceptClients", acceptClients); }, [acceptClients, form]); return ( <>
{!loadingPage && (
{t("siteInfo")}
{ if (e.key === "Enter") { e.preventDefault(); // block default enter refresh } }} className="space-y-4" id="create-site-form" > ( {t("name")} )} /> {env.flags.enableClients && form.watch("method") === "newt" && ( ( {t( "siteAddress" )} { setClientAddress( e .target .value ); field.onChange( e .target .value ); }} /> {t( "siteAddressDescription" )} )} /> )}
{tunnelTypes.length > 1 && ( {t("tunnelType")} {t("siteTunnelDescription")} { form.setValue("method", value); }} cols={3} /> )} {form.watch("method") === "newt" && ( <> {t("siteNewtCredentials")} {t( "siteNewtCredentialsDescription" )} {t("newtEndpoint")} {t("newtId")} {t("newtSecretKey")} {t("siteCredentialsSave")} {t( "siteCredentialsSaveDescription" )} {/*
*/} {/* */} {/* ( */} {/* */} {/*
*/} {/* { */} {/* form.setValue( */} {/* "copied", */} {/* e as boolean */} {/* ); */} {/* }} */} {/* /> */} {/* */} {/*
*/} {/* */} {/*
*/} {/* )} */} {/* /> */} {/* */} {/* */}
{t("siteInstallNewt")} {t("siteInstallNewtDescription")}

{t("operatingSystem")}

{platforms.map((os) => ( ))}

{["docker", "podman"].includes( platform ) ? t("method") : t("architecture")}

{getArchitectures().map( (arch) => ( ) )}

{t("siteConfiguration")}

{ const value = checked as boolean; setAcceptClients( value ); form.setValue( "acceptClients", value ); // Re-hydrate commands with new acceptClients value if ( newtId && newtSecret && newtVersion ) { hydrateCommands( newtId, newtSecret, env.app .dashboardUrl, newtVersion, value ); } }} label={t( "siteAcceptClientConnections" )} />

{t( "siteAcceptClientConnectionsDescription" )}

{t("commands")}

)} {form.watch("method") === "wireguard" && ( {t("WgConfiguration")} {t("WgConfigurationDescription")}
{t("siteCredentialsSave")} {t( "siteCredentialsSaveDescription" )}
)}
)} ); }