"use client"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { useToast } 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, SquareArrowOutUpRight } from "lucide-react"; const createSiteFormSchema = z.object({ name: z .string() .min(2, { message: "Name must be at least 2 characters." }) .max(30, { message: "Name must not be longer than 30 characters." }), method: z.enum(["wireguard", "newt", "local"]) }); type CreateSiteFormValues = z.infer; const defaultValues: Partial = { name: "", method: "newt" }; 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 { toast } = useToast(); const api = createApiClient(useEnvContext()); const { env } = useEnvContext(); const [isLoading, setIsLoading] = useState(false); const [isChecked, setIsChecked] = useState(false); const [keypair, setKeypair] = useState<{ publicKey: string; privateKey: string; } | null>(null); const [siteDefaults, setSiteDefaults] = useState(null); 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; // reset all values setLoading?.(false); setIsLoading(false); form.reset(); setChecked?.(false); setKeypair(null); setSiteDefaults(null); const generatedKeypair = generateKeypair(); setKeypair(generatedKeypair); 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); } }); }, [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: "Error creating site", description: "Key pair or site defaults not found" }); 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: "Error creating site", description: "Site defaults not found" }); setLoading?.(false); setIsLoading(false); return; } payload = { ...payload, subnet: siteDefaults.subnet, exitNodeId: siteDefaults.exitNodeId, secret: siteDefaults.newtSecret, newtId: siteDefaults.newtId }; } const res = await api .put>( `/org/${orgId}/site/`, payload ) .catch((e) => { toast({ variant: "destructive", title: "Error creating site", 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" ? "0 MB" : "--", mbOut: data.type == "wireguard" || data.type == "newt" ? "0 MB" : "--", 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}`; return (
( Name This is the name that will be displayed for this site. )} /> ( Method This is how you will expose connections. )} />
{form.watch("method") === "wireguard" && !isLoading ? ( <> You will only be able to see the configuration once. ) : form.watch("method") === "wireguard" && isLoading ? (

Loading WireGuard configuration...

) : form.watch("method") === "newt" ? ( <> You will only be able to see the configuration once. ) : null}
{form.watch("method") === "newt" && ( {" "} Learn how to install Newt on your system )} {form.watch("method") === "local" && ( {" "} Local sites do not tunnel, learn more )} {(form.watch("method") === "newt" || form.watch("method") === "wireguard") && (
)}
); }