"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 { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { CreateClientBody, CreateClientResponse, PickClientDefaultsResponse } from "@server/routers/client"; import { ListSitesResponse } from "@server/routers/site"; import { toast } from "@app/hooks/useToast"; import { AxiosResponse } from "axios"; import { useParams, useRouter } from "next/navigation"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { useTranslations } from "next-intl"; type ClientType = "olm"; interface TunnelTypeOption { id: ClientType; title: string; description: string; disabled?: boolean; } type Commands = { unix: Record; windows: Record; }; const platforms = ["unix", "windows"] 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 createClientFormSchema = z.object({ name: z .string() .min(2, { message: t("nameMin", { len: 2 }) }) .max(30, { message: t("nameMax", { len: 30 }) }), method: z.enum(["olm"]), siteIds: z .array( z.object({ id: z.string(), text: z.string() }) ) .refine((val) => val.length > 0, { message: t("siteRequired") }), subnet: z.string().ip().min(1, { message: t("subnetRequired") }) }); type CreateClientFormValues = z.infer; const [tunnelTypes, setTunnelTypes] = useState< ReadonlyArray >([ { id: "olm", title: t("olmTunnel"), description: t("olmTunnelDescription"), disabled: true } ]); const [loadingPage, setLoadingPage] = useState(true); const [sites, setSites] = useState([]); const [activeSitesTagIndex, setActiveSitesTagIndex] = useState< number | null >(null); const [platform, setPlatform] = useState("unix"); const [architecture, setArchitecture] = useState("All"); const [commands, setCommands] = useState(null); const [olmId, setOlmId] = useState(""); const [olmSecret, setOlmSecret] = useState(""); const [olmCommand, setOlmCommand] = useState(""); const [createLoading, setCreateLoading] = useState(false); const [clientDefaults, setClientDefaults] = useState(null); const hydrateCommands = ( id: string, secret: string, endpoint: string, version: string ) => { const commands = { unix: { All: [ `curl -fsSL https://pangolin.net/get-olm.sh | bash`, `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}` ] }, windows: { x64: [ `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`, `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}` ] } }; setCommands(commands); }; const getArchitectures = () => { switch (platform) { case "unix": return ["All"]; case "windows": return ["x64"]; default: return ["x64"]; } }; const getPlatformName = (platformName: string) => { switch (platformName) { case "windows": return "Windows"; case "unix": return "Unix & macOS"; case "docker": return "Docker"; default: return "Unix & macOS"; } }; 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 "unix": return ; case "docker": return ; case "kubernetes": return ; case "podman": return ; case "freebsd": return ; case "nixos": return ; default: return ; } }; const form = useForm({ resolver: zodResolver(createClientFormSchema), defaultValues: { name: "", method: "olm", siteIds: [], subnet: "" } }); async function onSubmit(data: CreateClientFormValues) { setCreateLoading(true); if (!clientDefaults) { toast({ variant: "destructive", title: t("errorCreatingClient"), description: t("clientDefaultsNotFound") }); setCreateLoading(false); return; } const payload: CreateClientBody = { name: data.name, type: data.method as "olm", siteIds: data.siteIds.map((site) => parseInt(site.id)), olmId: clientDefaults.olmId, secret: clientDefaults.olmSecret, subnet: data.subnet }; const res = await api .put< AxiosResponse >(`/org/${orgId}/client`, payload) .catch((e) => { toast({ variant: "destructive", title: t("errorCreatingClient"), description: formatAxiosError(e) }); }); if (res && res.status === 201) { const data = res.data.data; router.push(`/${orgId}/settings/clients/${data.clientId}`); } setCreateLoading(false); } useEffect(() => { const load = async () => { setLoadingPage(true); // Fetch available sites const res = await api.get>( `/org/${orgId}/sites/` ); const sites = res.data.data.sites.filter( (s) => s.type === "newt" && s.subnet ); setSites( sites.map((site) => ({ id: site.siteId.toString(), text: site.name })) ); let olmVersion = "latest"; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 3000); const response = await fetch( `https://api.github.com/repos/fosrl/olm/releases/latest`, { signal: controller.signal } ); clearTimeout(timeoutId); if (!response.ok) { throw new Error( t("olmErrorFetchReleases", { err: response.statusText }) ); } const data = await response.json(); const latestVersion = data.tag_name; olmVersion = latestVersion; } catch (error) { if (error instanceof Error && error.name === 'AbortError') { console.error(t("olmErrorFetchTimeout")); } else { console.error( t("olmErrorFetchLatest", { err: error instanceof Error ? error.message : String(error) }) ); } } await api .get(`/org/${orgId}/pick-client-defaults`) .catch((e) => { form.setValue("method", "olm"); }) .then((res) => { if (res && res.status === 200) { const data = res.data.data; setClientDefaults(data); const olmId = data.olmId; const olmSecret = data.olmSecret; const olmCommand = `olm --id ${olmId} --secret ${olmSecret} --endpoint ${env.app.dashboardUrl}`; setOlmId(olmId); setOlmSecret(olmSecret); setOlmCommand(olmCommand); hydrateCommands( olmId, olmSecret, env.app.dashboardUrl, olmVersion ); if (data.subnet) { form.setValue("subnet", data.subnet); } setTunnelTypes((prev: any) => { return prev.map((item: any) => { return { ...item, disabled: false }; }); }); } }); setLoadingPage(false); }; load(); }, []); return ( <>
{!loadingPage && (
{t("clientInformation")}
{ if (e.key === "Enter") { e.preventDefault(); // block default enter refresh } }} className="space-y-4" id="create-client-form" > ( {t("name")} )} /> ( {t("address")} {t("addressDescription")} )} /> ( {t("sites")} { form.setValue( "siteIds", olmags as [ Tag, ...Tag[] ] ); }} enableAutocomplete={ true } autocompleteOptions={ sites } allowDuplicates={ false } restrictTagsToAutocompleteOptions={ true } sortTags={true} /> {t("sitesDescription")} )} />
{form.watch("method") === "olm" && ( <> {t("clientOlmCredentials")} {t("clientOlmCredentialsDescription")} {t("olmEndpoint")} {t("olmId")} {t("olmSecretKey")} {t("clientCredentialsSave")} {t( "clientCredentialsSaveDescription" )} {t("clientInstallOlm")} {t("clientInstallOlmDescription")}

{t("operatingSystem")}

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

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

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

{t("commands")}

)}
)} ); }