"use client"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { toast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { useParams, useRouter } from "next/navigation"; import { CreateSiteBody, CreateSiteResponse, PickSiteDefaultsResponse } from "@server/routers/site"; import { generateKeypair } from "./[niceId]/wireguardConfig"; import CopyTextBox from "@app/components/CopyTextBox"; import { Checkbox } from "@app/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@app/components/ui/select"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { SiteRow } from "./SitesTable"; import { AxiosResponse } from "axios"; import { Button } from "@app/components/ui/button"; import Link from "next/link"; import { ArrowUpRight, ChevronsUpDown, Loader2, SquareArrowOutUpRight } from "lucide-react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@app/components/ui/collapsible"; import LoaderPlaceholder from "@app/components/PlaceHolderLoader"; import { useTranslations } from "next-intl"; type CreateSiteFormProps = { onCreate?: (site: SiteRow) => void; setLoading?: (loading: boolean) => void; setChecked?: (checked: boolean) => void; orgId: string; }; export default function CreateSiteForm({ onCreate, setLoading, setChecked, orgId }: CreateSiteFormProps) { const api = createApiClient(useEnvContext()); const { env } = useEnvContext(); const [isLoading, setIsLoading] = useState(false); const [isChecked, setIsChecked] = useState(false); const [isOpen, setIsOpen] = useState(false); const [keypair, setKeypair] = useState<{ publicKey: string; privateKey: string; } | null>(null); 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(["wireguard", "newt", "local"]) }); type CreateSiteFormValues = z.infer; const defaultValues: Partial = { name: "", method: "newt" }; const [siteDefaults, setSiteDefaults] = useState(null); const [loadingPage, setLoadingPage] = useState(true); const handleCheckboxChange = (checked: boolean) => { // setChecked?.(checked); setIsChecked(checked); }; const form = useForm({ resolver: zodResolver(createSiteFormSchema), defaultValues }); const nameField = form.watch("name"); const methodField = form.watch("method"); useEffect(() => { const nameIsValid = nameField?.length >= 2 && nameField?.length <= 30; const isFormValid = methodField === "local" || isChecked; // Only set checked to true if name is valid AND (method is local OR checkbox is checked) setChecked?.(nameIsValid && isFormValid); }, [nameField, methodField, isChecked, setChecked]); useEffect(() => { if (!open) return; const load = async () => { setLoadingPage(true); // reset all values setLoading?.(false); setIsLoading(false); form.reset(); setChecked?.(false); setKeypair(null); setSiteDefaults(null); const generatedKeypair = generateKeypair(); setKeypair(generatedKeypair); 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) { setSiteDefaults(res.data.data); } }); await new Promise((resolve) => setTimeout(resolve, 200)); setLoadingPage(false); }; load(); }, [open]); async function onSubmit(data: CreateSiteFormValues) { setLoading?.(true); setIsLoading(true); let payload: CreateSiteBody = { name: data.name, type: data.method }; if (data.method == "wireguard") { if (!keypair || !siteDefaults) { toast({ variant: "destructive", title: t('siteErrorCreate'), description: t('siteErrorCreateKeyPair') }); setLoading?.(false); setIsLoading(false); return; } payload = { ...payload, subnet: siteDefaults.subnet, exitNodeId: siteDefaults.exitNodeId, pubKey: keypair.publicKey }; } if (data.method === "newt") { if (!siteDefaults) { toast({ variant: "destructive", title: t('siteErrorCreate'), description: t('siteErrorCreateDefaults') }); setLoading?.(false); setIsLoading(false); return; } payload = { ...payload, subnet: siteDefaults.subnet, exitNodeId: siteDefaults.exitNodeId, secret: siteDefaults.newtSecret, newtId: siteDefaults.newtId }; } 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; onCreate?.({ name: data.name, id: data.siteId, nice: data.niceId.toString(), mbIn: data.type == "wireguard" || data.type == "newt" ? t('megabytes', {count: 0}) : "-", mbOut: data.type == "wireguard" || data.type == "newt" ? t('megabytes', {count: 0}) : "-", orgId: orgId as string, type: data.type as any, online: false }); } setLoading?.(false); setIsLoading(false); } const wgConfig = keypair && siteDefaults ? `[Interface] Address = ${siteDefaults.subnet} ListenPort = 51820 PrivateKey = ${keypair.privateKey} [Peer] PublicKey = ${siteDefaults.publicKey} AllowedIPs = ${siteDefaults.address.split("/")[0]}/32 Endpoint = ${siteDefaults.endpoint}:${siteDefaults.listenPort} PersistentKeepalive = 5` : ""; const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`; const newtConfigDockerCompose = `services: newt: image: fosrl/newt container_name: newt restart: unless-stopped environment: - PANGOLIN_ENDPOINT=${env.app.dashboardUrl} - NEWT_ID=${siteDefaults?.newtId} - NEWT_SECRET=${siteDefaults?.newtSecret}`; const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`; return loadingPage ? ( ) : (
( {t('name')} {t('siteNameDescription')} )} /> ( {t('method')} {t('siteMethodDescription')} )} /> {form.watch("method") === "newt" && ( {t('siteLearnNewt')} )}
{form.watch("method") === "wireguard" && !isLoading ? ( <> {t('siteSeeConfigOnce')} ) : form.watch("method") === "wireguard" && isLoading ? (

{t('siteLoadWGConfig')}

) : form.watch("method") === "newt" && siteDefaults ? ( <>
{t('siteSeeConfigOnce')}
{t('dockerCompose')}
{t('dockerRun')}
) : null}
{form.watch("method") === "local" && ( {t('siteLearnLocal')} )} {(form.watch("method") === "newt" || form.watch("method") === "wireguard") && (
)}
); }