'use client'; import { getNodeConfig, updateNodeConfig } from '@/services/admin/system'; 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/custom-components/dynamic-Inputs'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { Icon } from '@workspace/ui/custom-components/icon'; import { unitConversion } from '@workspace/ui/utils'; import { DicesIcon } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { uid } from 'radash'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; 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 = useTranslations('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')); await refetchCfg(); setOpen(false); } finally { setSaving(false); } } return (

{t('server_config.title')}

{t('server_config.description')}

{t('server_config.title')} {t('server_config.tabs.basic')} {t('server_config.tabs.dns')} {t('server_config.tabs.outbound')} {t('server_config.tabs.block')}
( {t('server_config.fields.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); }} className='cursor-pointer' /> } /> {t('server_config.fields.communication_key_desc')} )} /> ( {t('server_config.fields.node_pull_interval')} {t('server_config.fields.node_pull_interval_desc')} )} /> ( {t('server_config.fields.node_push_interval')} {t('server_config.fields.node_push_interval_desc')} )} /> ( {t('server_config.fields.traffic_report_threshold')} { field.onChange(unitConversion('mbToBits', value)); }} placeholder='1' /> {t('server_config.fields.traffic_report_threshold_desc')} )} /> ( {t('server_config.fields.ip_strategy')} {t('server_config.fields.ip_strategy_desc')} )} /> ( {t('server_config.fields.dns_config')} ({ ...item, domains: Array.isArray(item.domains) ? item.domains.join('\n') : '', }))} onChange={(values) => { 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); }} /> )} /> { return ( ({ label: cipher, value: cipher, })), visible: (item: Record) => item.protocol === 'shadowsocks', }, { name: 'address', type: 'text', placeholder: t( 'server_config.fields.outbound_address_placeholder', ), }, { name: 'port', type: 'number', placeholder: t('server_config.fields.outbound_port_placeholder'), }, { name: 'password', type: 'text', placeholder: t( 'server_config.fields.outbound_password_placeholder', ), }, { name: 'rules', type: 'textarea', className: 'col-span-2', placeholder: t('server_config.fields.outbound_rules_placeholder'), }, ]} value={(field.value || []).map((item) => ({ ...item, rules: Array.isArray(item.rules) ? item.rules.join('\n') : '', }))} 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); }} /> ); }} /> (