"use client"; import { SettingsFormCell, SettingsFormGrid, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; import HeaderTitle from "@app/components/SettingsSectionTitle"; import { OptionSelect, type OptionSelectOption } from "@app/components/OptionSelect"; import DomainPicker from "@app/components/DomainPicker"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { Button } from "@app/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import type { Selectedsite } from "@app/components/site-selector"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { buildCreateSiteResourcePayload, createCreateFormSchema, type PrivateResourceMode } from "@app/lib/privateResourceForm"; import { zodResolver } from "@hookform/resolvers/zod"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import type { SiteResource } from "@server/db"; import { GetSiteResponse } from "@server/routers/site/getSite"; import type ResponseT from "@server/types/Response"; import { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useEffect, useMemo, useState, useTransition } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField"; import { PrivateResourceHttpFields } from "@app/components/PrivateResourceHttpFields"; import { PrivateResourceSshFields } from "@app/components/PrivateResourceSshFields"; import { PrivateResourcePortRanges } from "@app/components/PrivateResourcePortRanges"; import { PrivateResourceAliasField, PrivateResourceCidrDestinationField, PrivateResourceHostDestinationFields } from "@app/components/PrivateResourceDestinationFields"; import { asAnyControl, asAnySetValue, asAnyWatch } from "@app/lib/formControlUtils"; export default function CreatePrivateResourcePage() { const params = useParams(); const searchParams = useSearchParams(); const router = useRouter(); const t = useTranslations(); const { env } = useEnvContext(); const api = createApiClient({ env }); const orgId = params.orgId as string; const disableEnterpriseFeatures = env.flags.disableEnterpriseFeatures; const { isPaidUser } = usePaidStatus(); const httpSectionDisabled = !isPaidUser( tierMatrix.advancedPrivateResources ); const sshSectionDisabled = !isPaidUser(tierMatrix.advancedPrivateResources); const [isSubmitting, startTransition] = useTransition(); const siteIdParam = searchParams.get("siteId"); const siteIdNumber = siteIdParam && Number.isInteger(Number(siteIdParam)) ? Number(siteIdParam) : null; const [selectedSites, setSelectedSites] = useState([]); const formSchema = useMemo(() => createCreateFormSchema(t), [t]); type FormValues = z.infer; const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { name: "", siteIds: [], mode: "host", destination: "", alias: null, destinationPort: null, scheme: "http", ssl: true, httpConfigSubdomain: null, httpConfigDomainId: null, httpConfigFullDomain: null, authDaemonMode: "native", standardDaemonLocation: "site", authDaemonPort: null, pamMode: "passthrough", tcpPortRangeString: "*", udpPortRangeString: "*", disableIcmp: false } }); useEffect(() => { if (!siteIdNumber) return; void api .get>(`/site/${siteIdNumber}`) .then((res) => { const site = res.data.data; if (!site || site.orgId !== orgId) return; const selected: Selectedsite = { siteId: site.siteId, name: site.name, type: site.type as Selectedsite["type"] }; setSelectedSites([selected]); form.setValue("siteIds", [site.siteId]); }) .catch(() => {}); }, [api, form, orgId, siteIdNumber]); const mode = form.watch("mode"); const authDaemonMode = form.watch("authDaemonMode"); const isNativeSsh = mode === "ssh" && authDaemonMode === "native"; const modeOptions: OptionSelectOption[] = [ { value: "host", label: t("createInternalResourceDialogModeHost") }, { value: "cidr", label: t("createInternalResourceDialogModeCidr") }, ...(!disableEnterpriseFeatures ? [ { value: "http" as const, label: t("createInternalResourceDialogModeHttp") }, { value: "ssh" as const, label: t("createInternalResourceDialogModeSsh") } ] : []) ]; const submitDisabled = isSubmitting || (mode === "http" && httpSectionDisabled) || (mode === "ssh" && sshSectionDisabled); function onSubmit(values: FormValues) { startTransition(async () => { try { const res = await api.put< AxiosResponse> >( `/org/${orgId}/site-resource`, buildCreateSiteResourcePayload({ ...values, destination: values.destination?.trim() && values.destination.trim().length > 0 ? values.destination.trim() : null }) ); toast({ title: t("createInternalResourceDialogSuccess"), description: t( "createInternalResourceDialogInternalResourceCreatedSuccessfully" ) }); const created = (res.data as unknown as ResponseT) .data; if (!created) { throw new Error("Failed to create private resource"); } router.push( `/${orgId}/settings/resources/private/${created.niceId}/${created.mode}` ); } catch (error) { toast({ title: t("createInternalResourceDialogError"), description: formatAxiosError( error, t( "createInternalResourceDialogFailedToCreateInternalResource" ) ), variant: "destructive" }); } }); } return ( <>
{/* General */} {t("resourceCreateGeneral")} {t("resourceCreateGeneralDescription")} ( {t("name")} {t( "resourceNameDescription" )} )} /> ( {t("type")} options={modeOptions} value={field.value} onChange={(newMode) => { field.onChange( newMode ); if ( newMode === "ssh" ) { form.setValue( "authDaemonMode", "native" ); form.setValue( "standardDaemonLocation", "site" ); form.setValue( "destination", null ); form.setValue( "destinationPort", null ); } else if ( newMode === "http" ) { form.setValue( "destinationPort", 443 ); } else { form.setValue( "destinationPort", null ); } }} cols={4} /> )} /> {mode === "http" && ( { if (!res) { form.setValue( "httpConfigSubdomain", null ); form.setValue( "httpConfigDomainId", null ); form.setValue( "httpConfigFullDomain", null ); return; } form.setValue( "httpConfigSubdomain", res.subdomain ?? null ); form.setValue( "httpConfigDomainId", res.domainId ); form.setValue( "httpConfigFullDomain", res.fullDomain ); }} /> {t( "resourceDomainDescription" )} )} {(mode === "host" || (mode === "ssh" && !isNativeSsh)) && ( )} {/* Host destination */} {mode === "host" && ( {t("hostSettings")} {t( "editInternalResourceDialogDestinationDescription" )} )} {/* CIDR destination */} {mode === "cidr" && ( {t("cidrSettings")} {t( "editInternalResourceDialogDestinationCidrDescription" )} )} {/* HTTP configuration */} {mode === "http" && ( {t("httpSettings")} {t( "editInternalResourceDialogHttpConfigurationDescription" )}
)} {/* SSH server */} {mode === "ssh" && ( {t("sshServer")} {t("sshServerDescription")}
)}
); }