"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; import { Button } from "@workspace/ui/components/button"; import { Card, CardContent } from "@workspace/ui/components/card"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@workspace/ui/components/form"; import { ScrollArea } from "@workspace/ui/components/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@workspace/ui/components/select"; import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, } from "@workspace/ui/components/sheet"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@workspace/ui/components/tabs"; import { Textarea } from "@workspace/ui/components/textarea"; import { ArrayInput } from "@workspace/ui/composed/dynamic-Inputs"; import { EnhancedInput } from "@workspace/ui/composed/enhanced-input"; import { Icon } from "@workspace/ui/composed/icon"; import { getNodeConfig, updateNodeConfig, } from "@workspace/ui/services/admin/system"; import { unitConversion } from "@workspace/ui/utils/unit-conversions"; import { DicesIcon } from "lucide-react"; import { uid } from "radash"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { z } from "zod"; import { SS_CIPHERS } from "./form-schema"; const dnsConfigSchema = z.object({ proto: z.string(), // z.enum(['tcp', 'udp', 'tls', 'https', 'quic']), address: z.string(), domains: z.array(z.string()), }); const outboundConfigSchema = z.object({ name: z.string(), protocol: z.string(), address: z.string(), port: z.number(), cipher: z.string().optional(), password: z.string().optional(), rules: z.array(z.string()).optional(), }); const nodeConfigSchema = z.object({ node_secret: z.string().optional(), node_pull_interval: z.number().optional(), node_push_interval: z.number().optional(), traffic_report_threshold: z.number().optional(), ip_strategy: z.enum(["prefer_ipv4", "prefer_ipv6"]).optional(), dns: z.array(dnsConfigSchema).optional(), block: z.array(z.string()).optional(), outbound: z.array(outboundConfigSchema).optional(), }); type NodeConfigFormData = z.infer; export default function ServerConfig() { const { t } = useTranslation("servers"); const [open, setOpen] = useState(false); const [saving, setSaving] = useState(false); const { data: cfgResp, refetch: refetchCfg } = useQuery({ queryKey: ["getNodeConfig"], queryFn: async () => { const { data } = await getNodeConfig(); return data.data as API.NodeConfig | undefined; }, enabled: open, }); const form = useForm({ resolver: zodResolver(nodeConfigSchema), defaultValues: { node_secret: "", node_pull_interval: undefined, node_push_interval: undefined, traffic_report_threshold: undefined, ip_strategy: "prefer_ipv4", dns: [], block: [], outbound: [], }, }); useEffect(() => { if (cfgResp) { form.reset({ node_secret: cfgResp.node_secret ?? "", node_pull_interval: cfgResp.node_pull_interval as number | undefined, node_push_interval: cfgResp.node_push_interval as number | undefined, traffic_report_threshold: cfgResp.traffic_report_threshold as | number | undefined, ip_strategy: (cfgResp.ip_strategy as "prefer_ipv4" | "prefer_ipv6" | undefined) || "prefer_ipv4", dns: cfgResp.dns || [], block: cfgResp.block || [], outbound: cfgResp.outbound || [], }); } }, [cfgResp, form]); async function onSubmit(values: NodeConfigFormData) { setSaving(true); try { await updateNodeConfig(values as API.NodeConfig); toast.success(t("server_config.saveSuccess", "Saved successfully")); await refetchCfg(); setOpen(false); } finally { setSaving(false); } } return (

{t("server_config.title", "Node configuration")}

{t( "server_config.description", "Manage node communication keys, pull/push intervals." )}

{t("server_config.title", "Node configuration")} {t("server_config.tabs.basic", "Basic Configuration")} {t("server_config.tabs.dns", "DNS Configuration")} {t("server_config.tabs.outbound", "Outbound Rules")} {t("server_config.tabs.block", "Block Rules")}
( {t( "server_config.fields.communication_key", "Communication key" )} { const id = uid(32).toLowerCase(); const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`; form.setValue("node_secret", formatted); }} /> } value={field.value || ""} /> {t( "server_config.fields.communication_key_desc", "Used for node authentication." )} )} /> ( {t( "server_config.fields.node_pull_interval", "Node pull interval" )} {t( "server_config.fields.node_pull_interval_desc", "How often the node pulls configuration (seconds)." )} )} /> ( {t( "server_config.fields.node_push_interval", "Node push interval" )} {t( "server_config.fields.node_push_interval_desc", "How often the node pushes stats (seconds)." )} )} /> ( {t( "server_config.fields.traffic_report_threshold", "Traffic Report Threshold" )} { field.onChange(unitConversion("mbToBits", value)); }} placeholder="1" suffix="MB" type="number" value={unitConversion( "bitsToMb", field.value as number | undefined )} /> {t( "server_config.fields.traffic_report_threshold_desc", "Set the minimum threshold for traffic reporting." )} )} /> ( {t("server_config.fields.ip_strategy", "IP Strategy")} {t( "server_config.fields.ip_strategy_desc", "Choose IP version preference for network connections" )} )} /> ( {t( "server_config.fields.dns_config", "DNS Configuration" )} { const converted = values.map((item: any) => ({ proto: item.proto, address: item.address, domains: typeof item.domains === "string" ? item.domains .split("\n") .map((d: string) => d.trim()) : item.domains || [], })); field.onChange(converted); }} value={(field.value || []).map((item) => ({ ...item, domains: Array.isArray(item.domains) ? item.domains.join("\n") : "", }))} /> )} /> ( ({ label: cipher, value: cipher, })), visible: (item: Record) => item.protocol === "shadowsocks", }, { name: "address", type: "text", placeholder: t( "server_config.fields.outbound_address_placeholder", "Server address" ), }, { name: "port", type: "number", placeholder: t( "server_config.fields.outbound_port_placeholder", "Port number" ), }, { name: "password", type: "text", placeholder: t( "server_config.fields.outbound_password_placeholder", "Password (optional)" ), }, { name: "rules", type: "textarea", className: "col-span-2", placeholder: t( "server_config.fields.outbound_rules_placeholder", "One rule per line" ), }, ]} onChange={(values) => { const converted = values.map((item: any) => ({ name: item.name, protocol: item.protocol, address: item.address, port: item.port, cipher: item.cipher, password: item.password, rules: typeof item.rules === "string" ? item.rules .split("\n") .map((r: string) => r.trim()) : item.rules || [], })); field.onChange(converted); }} value={(field.value || []).map((item) => ({ ...item, rules: Array.isArray(item.rules) ? item.rules.join("\n") : "", }))} /> )} /> (