diff --git a/apps/admin/app/dashboard/server/form-schema.ts b/apps/admin/app/dashboard/server/form-schema.ts new file mode 100644 index 0000000..84603bc --- /dev/null +++ b/apps/admin/app/dashboard/server/form-schema.ts @@ -0,0 +1,98 @@ +import { z } from '@shadcn/ui/lib/zod'; + +export const protocols = ['shadowsocks', 'vmess', 'vless', 'trojan', 'hysteria2', 'tuic']; + +const nullableString = z.string().nullish(); +const portSchema = z.number().min(1).max(65535); +const securityConfigSchema = z + .object({ + sni: nullableString, + allow_insecure: z.boolean().nullable().default(false), + fingerprint: nullableString, + reality_private_key: nullableString, + reality_public_key: nullableString, + reality_short_id: nullableString, + }) + .nullish(); + +const transportConfigSchema = z + .object({ + path: nullableString, + host: nullableString, + server_name: nullableString, + }) + .nullish(); + +const baseProtocolSchema = z.object({ + port: portSchema, + transport: z.string(), + transport_config: transportConfigSchema, + security: z.string(), + security_config: securityConfigSchema, +}); + +const shadowsocksSchema = z.object({ + method: z.string(), + port: portSchema, +}); + +const vmessSchema = baseProtocolSchema; + +const vlessSchema = baseProtocolSchema.extend({ + flow: nullableString, +}); + +const trojanSchema = baseProtocolSchema; + +const hysteria2Schema = z.object({ + port: portSchema, + hop_ports: nullableString, + hop_interval: z.number().nullish(), + obfs_password: nullableString, + security_config: securityConfigSchema, +}); + +const tuicSchema = z.object({ + port: portSchema, + security_config: securityConfigSchema, +}); + +const protocolConfigSchema = z.discriminatedUnion('protocol', [ + z.object({ + protocol: z.literal('shadowsocks'), + config: shadowsocksSchema, + }), + z.object({ + protocol: z.literal('vmess'), + config: vmessSchema, + }), + z.object({ + protocol: z.literal('vless'), + config: vlessSchema, + }), + z.object({ + protocol: z.literal('trojan'), + config: trojanSchema, + }), + z.object({ + protocol: z.literal('hysteria2'), + config: hysteria2Schema, + }), + z.object({ + protocol: z.literal('tuic'), + config: tuicSchema, + }), +]); + +const baseFormSchema = z.object({ + name: z.string(), + server_addr: z.string(), + speed_limit: z.number().nullish(), + traffic_ratio: z.number().default(1), + group_id: z.number().nullish(), + enable_relay: z.boolean().nullish().default(false), + relay_host: nullableString, + relay_port: z.number().nullish(), +}); + +export const formSchema = z.intersection(baseFormSchema, protocolConfigSchema); diff --git a/apps/admin/app/dashboard/server/node-form.tsx b/apps/admin/app/dashboard/server/node-form.tsx index 0cf712d..bf4c87c 100644 --- a/apps/admin/app/dashboard/server/node-form.tsx +++ b/apps/admin/app/dashboard/server/node-form.tsx @@ -3,13 +3,14 @@ import { getNodeGroupList } from '@/services/admin/server'; import { Icon } from '@iconify/react'; import { Combobox } from '@repo/ui/combobox'; -import { JSONEditor } from '@repo/ui/editor'; import { EnhancedInput } from '@repo/ui/enhanced-input'; import { unitConversion } from '@repo/ui/utils'; import { Button } from '@shadcn/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@shadcn/ui/card'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@shadcn/ui/form'; import { useForm } from '@shadcn/ui/lib/react-hook-form'; -import { z, zodResolver } from '@shadcn/ui/lib/zod'; +import { cn } from '@shadcn/ui/lib/utils'; +import { zodResolver } from '@shadcn/ui/lib/zod'; import { ScrollArea } from '@shadcn/ui/scroll-area'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@shadcn/ui/select'; import { @@ -25,64 +26,7 @@ import { Tabs, TabsList, TabsTrigger } from '@shadcn/ui/tabs'; import { useQuery } from '@tanstack/react-query'; import { useTranslations } from 'next-intl'; import { useEffect, useState } from 'react'; - -const shadowsocksSchema = z.object({ - method: z.string(), - port: z.number(), - enable_relay: z.boolean().nullish(), - relay_host: z.string().nullish(), - relay_port: z.number().nullish(), -}); - -const vmessSchema = z.object({ - host: z.string(), - port: z.number(), - enable_tls: z.boolean().nullish(), - tls_config: z.any().nullable(), - network: z.string(), - transport: z.any().nullable(), - enable_relay: z.boolean().nullish(), - relay_host: z.string().nullish(), - relay_port: z.number().nullish(), -}); - -const vlessSchema = z.object({ - host: z.string(), - port: z.number(), - network: z.string(), - transport: z.any().nullable(), - security: z.string(), - security_config: z.any().nullable(), - xtls: z.string().nullish(), - enable_relay: z.boolean().nullish(), - relay_host: z.string().nullish(), - relay_port: z.number().nullish(), -}); - -const trojanSchema = z.object({ - host: z.string(), - port: z.number(), - network: z.string(), - sni: z.string().nullish(), - allow_insecure: z.boolean().nullish(), - transport: z.any().nullable(), - enable_relay: z.boolean().nullish(), - relay_host: z.string().nullish(), - relay_port: z.number().nullish(), -}); - -const formSchema = z.object({ - name: z.string(), - server_addr: z.string(), - speed_limit: z.number().nullish(), - traffic_ratio: z.number(), - groupId: z.number().nullish(), - protocol: z.enum(['shadowsocks', 'vmess', 'vless', 'trojan']), - vmess: vmessSchema.nullish(), - vless: vlessSchema.nullish(), - trojan: trojanSchema.nullish(), - shadowsocks: shadowsocksSchema.nullish(), -}); +import { formSchema, protocols } from './form-schema'; interface NodeFormProps { onSubmit: (data: T) => Promise | boolean; @@ -107,49 +51,24 @@ export default function NodeForm({ defaultValues: { traffic_ratio: 1, protocol: 'shadowsocks', - ...initialValues, + config: { + security: 'none', + transport: 'tcp', + ...initialValues?.config, + }, } as any, }); const protocol = form.watch('protocol'); - const vmessNetwork = form.watch('vmess.network'); - const vlessNetwork = form.watch('vless.network'); - const trojanNetwork = form.watch('trojan.network'); + const transport = form.watch('config.transport'); + const security = form.watch('config.security'); useEffect(() => { form?.reset(initialValues); }, [form, initialValues]); async function handleSubmit(data: { [x: string]: any }) { - let newData = {}; - switch (data.protocol) { - case 'vmess': - newData = { - tls_config: {}, - transport: {}, - enable_relay: false, - }; - break; - case 'vless': - newData = { - security_config: {}, - transport: {}, - enable_relay: false, - }; - break; - case 'trojan': - newData = { - transport: {}, - allow_insecure: false, - enable_relay: false, - }; - break; - } - const bool = await onSubmit({ - ...newData, - ...data, - } as unknown as T); - + const bool = await onSubmit(data as unknown as T); if (bool) setOpen(false); } @@ -201,7 +120,7 @@ export default function NodeForm({ /> ( {t('form.nodeGroupId')} @@ -282,6 +201,63 @@ export default function NodeForm({ )} /> + ( + + {t('form.enableRelay')} + +
+ { + form.setValue(field.name, value); + }} + /> +
+
+ +
+ )} + /> + ( + + {t('form.relayHost')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + ( + + {t('form.relayPort')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> ({ value={field.value} onValueChange={(value) => { form.setValue(field.name, value); - const protocols = ['shadowsocks', 'vmess', 'vless', 'trojan']; - protocols.forEach((proto) => { - if (proto !== value) { - form.setValue(proto, undefined); - } - }); - const host = form.getValues().server_addr; - if (value !== 'shadowsocks') form.setValue(`${value}.host`, host); - if (value === 'vless') { - form.setValue('vless.security', 'none'); - form.setValue('vless.xtls', 'none'); + if (['trojan', 'hysteria2', 'tuic'].includes(value)) { + form.setValue('config.security', 'tls'); } }} > - - Shadowsocks - Vmess - Vless - Trojan + + {protocols.map((proto) => ( + + {proto.charAt(0).toUpperCase() + proto.slice(1)} + + ))} @@ -326,7 +294,7 @@ export default function NodeForm({
( {t('form.encryptionMethod')} @@ -367,7 +335,7 @@ export default function NodeForm({ /> ( {t('form.port')} @@ -384,1133 +352,474 @@ export default function NodeForm({ )} /> - ( - - {t('form.enableRelay')} - -
- { +
+ )} + + {['vmess', 'vless', 'trojan', 'hysteria2', 'tuic'].includes(protocol) && ( +
+
+ ( + + {t('form.port')} + + { form.setValue(field.name, value); }} /> -
- - - - )} - /> - ( - - {t('form.relayHost')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - {t('form.relayPort')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> -
- )} - {protocol === 'vmess' && ( -
- ( - - {t('form.Host Name')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - {t('form.port')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - {t('form.networkType')} - - { + form.setValue(field.name, value); + }} + > + + + + + + + NONE + xtls-rprx-vision + + - - TCP - WebSocket - gRPC - QUIC - mkcp - HTTPUPgrade - SplitHTTP - - - - - + + + )} + /> )} - /> - ( - - - {t('form.enableTls')} - - - {t('form.edit')} - - - - {t('form.editSecurity')} - -
- ( - - {t('form.serverName')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} + {protocol === 'hysteria2' && ( + <> + ( + + {t('form.obfsPassword')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + ( + + {t('form.hopPorts')} + + { + form.setValue(field.name, value); + }} /> - ( - - {t('form.allowInsecure')} - -
- { - form.setValue(field.name, value); - }} - /> -
-
- -
- )} +
+ +
+ )} + /> + ( + + {t('form.hopInterval')} + + { + form.setValue(field.name, value); + }} + suffix='S' /> -
-
-
-
- -
- { - form.setValue(field.name, value); - }} - /> -
-
- -
+ + + + )} + /> + )} - /> - { - const placeholders: any = { - tcp: { - header: { - type: 'http', - request: { - path: ['/'], - headers: { - Host: ['www.baidu.com', 'www.bing.com'], - }, - }, - response: {}, - }, - }, - websocket: { - path: '/', - headers: { - Host: 'ppanel.dev', - }, - }, - grpc: { - serviceName: 'GunService', - }, - quic: { - security: 'none', - key: '', - header: { - type: 'none', - }, - }, - mkcp: { - header: { - type: 'none', - }, - seed: '', - }, - httpupgrade: { - path: '/', - host: 'xtls.github.io', - }, - splithttp: { - path: '/', - host: 'xtls.github.io', - }, - }; - return ( - - {/* {t('form.transport')} */} - - + {['vmess', 'vless', 'trojan'].includes(protocol) && ( + + + {t('form.transportConfig')} + ( + + + + + + + )} + /> + + {transport !== 'tcp' && ( + + {['websocket', 'http2', 'httpupgrade'].includes(transport) && ( + <> + ( + + PATH + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + ( + + HOST + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + + )} + {['grpc'].includes(transport) && ( + ( + + Server Name + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + )} + + )} + + )} + + + {t('form.securityConfig')} + ( + + + + + )} + /> + + {security !== 'none' && ( + + ( + + Server Name(SNI) + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + {protocol === 'vless' && security === 'reality' && ( + <> + ( + + {t('form.security_config.serverAddress')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} /> - - - - ); - }} - /> - ( - - {t('form.enableRelay')} - -
- { - form.setValue(field.name, value); - }} + ( + + {t('form.security_config.serverPort')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} /> -
-
- -
- )} - /> - ( - - {t('form.relayHost')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - {t('form.relayPort')} - - { - form.setValue(field.name, value); - }} + ( + + {t('form.security_config.proxyProtocol')} + + + + )} + /> + ( + + {t('form.security_config.privateKey')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + ( + + {t('form.security_config.publicKey')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + ( + + {t('form.security_config.shortId')} + + { + form.setValue(field.name, value); + }} + /> + + + + )} + /> + + )} + {protocol === 'vless' && ( + ( + + {t('form.security_config.fingerprint')} + + + + + )} /> - - - + )} + ( + + Allow Insecure + +
+ { + form.setValue(field.name, value); + }} + /> +
+
+ +
+ )} + /> + )} - /> -
- )} - {protocol === 'vless' && ( -
- ( - - {t('form.Host Name')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - {t('form.port')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - - {t('form.security')} - {field.value && field.value !== 'none' && ( - - - {t('form.edit')} - - - - {t('form.editSecurity')} - -
- {field.value === 'tls' && ( - <> - ( - - - {t('form.security_config.serverName')} - - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - - {t('form.security_config.fingerprint')} - - - - - - )} - /> - ( - - {t('form.allowInsecure')} - -
- { - form.setValue(field.name, value); - }} - /> -
-
- -
- )} - /> - - )} - {field.value === 'reality' && ( - <> - ( - - - {t('form.security_config.serverName')} - - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - - {t('form.security_config.serverAddress')} - - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - - {t('form.security_config.serverPort')} - - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - - {t('form.security_config.proxyProtocol')} - - - - - )} - /> - ( - - - {t('form.security_config.privateKey')} - - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - - {t('form.security_config.publicKey')} - - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - - {t('form.security_config.shortId')} - - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - - {t('form.security_config.fingerprint')} - - - - - - )} - /> - ( - - {t('form.allowInsecure')} - -
- { - form.setValue(field.name, value); - }} - /> -
-
- -
- )} - /> - - )} -
-
-
- )} -
- - -
- )} - /> - ( - - {t('form.networkType')} - - - - - - )} - /> - ( - - {t('form.xtls')} - - - - - - )} - /> - { - const placeholders: any = { - tcp: { - header: { - type: 'http', - request: { - path: ['/'], - headers: { - Host: ['www.baidu.com', 'www.bing.com'], - }, - }, - response: {}, - }, - }, - websocket: { - path: '/', - headers: { - Host: 'ppanel.dev', - }, - }, - grpc: { - serviceName: 'GunService', - }, - http2: { - path: '/', - host: 'xtls.github.io', - }, - quic: { - security: 'none', - key: '', - header: { - type: 'none', - }, - }, - mkcp: { - header: { - type: 'none', - }, - seed: '', - }, - httpupgrade: { - path: '/', - host: 'xtls.github.io', - }, - splithttp: { - path: '/', - host: 'xtls.github.io', - }, - }; - return ( - - - { - form.setValue(field.name, value); - }} - /> - - - - ); - }} - /> - ( - - {t('form.enableRelay')} - -
- { - form.setValue(field.name, value); - }} - /> -
-
- -
- )} - /> - ( - - {t('form.relayHost')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - {t('form.relayPort')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> -
- )} - {protocol === 'trojan' && ( -
- ( - - {t('form.Host Name')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - {t('form.port')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - {t('form.allowInsecure')} - -
- { - form.setValue(field.name, value); - }} - /> -
-
- -
- )} - /> - ( - - {t('form.sni')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - {t('form.networkType')} - - - - - - )} - /> - { - const placeholders: any = { - tcp: {}, - websocket: { - path: '/', - headers: { - Host: 'ppanel.dev', - }, - }, - grpc: { - serviceName: 'GunService', - }, - }; - return ( - - - { - form.setValue(field.name, value); - }} - /> - - - - ); - }} - /> - ( - - {t('form.enableRelay')} - -
- { - form.setValue(field.name, value); - }} - /> -
-
- -
- )} - /> - ( - - {t('form.relayHost')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> - ( - - {t('form.relayPort')} - - { - form.setValue(field.name, value); - }} - /> - - - - )} - /> +
)} @@ -1529,6 +838,7 @@ export default function NodeForm({