'use client'; import { useEffect } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useQuery } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import type { ProxyRouteItem } from '@/lib/services/openflare'; import { NodeService, PagesService } from '@/lib/services/openflare'; import { customHeadersToText, parseCustomHeadersText, parseOriginUrl, parseOriginUrls, validateOriginHost, } from '../../components/helpers'; import { proxyRouteFormIds } from '../helpers'; import { useRouteSectionSave } from '../hooks/use-route-section-save'; import { SectionShell } from './section-shell'; const reverseProxySchema = z .object({ upstream_type: z.enum(['direct', 'tunnel', 'pages']), origin_urls_text: z.string().trim(), origin_host: z.string(), tunnel_id: z.string().optional(), tunnel_target_addr: z.string().trim().optional(), tunnel_target_protocol: z.enum(['http', 'https']).optional(), pages_project_id: z.string().optional(), custom_headers_text: z.string(), }) .superRefine((value, context) => { if (value.upstream_type === 'direct') { if (!value.origin_urls_text.trim()) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['origin_urls_text'], message: '请至少填写一个上游地址', }); } else { const { error } = parseOriginUrls(value.origin_urls_text); if (error) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['origin_urls_text'], message: error, }); } } } else if (value.upstream_type === 'tunnel') { if (!value.tunnel_id) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['tunnel_id'], message: '请选择内网穿透隧道', }); } if (!value.tunnel_target_addr) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['tunnel_target_addr'], message: '请填写内网服务地址 (如 127.0.0.1:8080)', }); } } else if (!value.pages_project_id) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['pages_project_id'], message: '请选择 Pages 项目', }); } const originHostError = validateOriginHost(value.origin_host); if (originHostError) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['origin_host'], message: originHostError, }); } const { error: headerError } = parseCustomHeadersText( value.custom_headers_text, ); if (headerError) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['custom_headers_text'], message: headerError, }); } }); type ReverseProxyValues = z.infer; interface ProxySectionProps { route: ProxyRouteItem; onRouteUpdate: (route: ProxyRouteItem) => void; onSavingChange?: (saving: boolean) => void; } export function ProxySection({ route, onRouteUpdate, onSavingChange, }: ProxySectionProps) { const { saving, save } = useRouteSectionSave( route, onRouteUpdate, onSavingChange, ); const tunnelsQuery = useQuery({ queryKey: ['openflare', 'nodes'], queryFn: () => NodeService.listNodes(), }); const pagesProjectsQuery = useQuery({ queryKey: ['openflare', 'pages-projects'], queryFn: () => PagesService.listProjects(), }); const tunnelClients = (tunnelsQuery.data ?? []).filter( (node) => node.node_type === 'tunnel_client', ); const pagesProjects = (pagesProjectsQuery.data ?? []).filter( (project) => project.enabled && project.active_deployment_id, ); const form = useForm({ resolver: zodResolver(reverseProxySchema), defaultValues: { upstream_type: route.upstream_type || 'direct', origin_urls_text: route.upstream_list.join('\n'), origin_host: route.origin_host || '', tunnel_id: route.tunnel_node_id ? String(route.tunnel_node_id) : '', tunnel_target_addr: route.tunnel_target_addr || '', tunnel_target_protocol: (route.tunnel_target_protocol as 'http' | 'https') || 'http', pages_project_id: route.pages_project_id ? String(route.pages_project_id) : '', custom_headers_text: customHeadersToText(route.custom_header_list), }, }); useEffect(() => { form.reset({ upstream_type: route.upstream_type || 'direct', origin_urls_text: route.upstream_list.join('\n'), origin_host: route.origin_host || '', tunnel_id: route.tunnel_node_id ? String(route.tunnel_node_id) : '', tunnel_target_addr: route.tunnel_target_addr || '', tunnel_target_protocol: (route.tunnel_target_protocol as 'http' | 'https') || 'http', pages_project_id: route.pages_project_id ? String(route.pages_project_id) : '', custom_headers_text: customHeadersToText(route.custom_header_list), }); }, [form, route]); const upstreamType = form.watch('upstream_type'); return (
{ let originUrl = ''; let originScheme: 'http' | 'https' = 'http'; let originAddress = ''; let originPort = ''; let originUri = ''; let upstreams: string[] = []; if (values.upstream_type === 'direct') { const { urls } = parseOriginUrls(values.origin_urls_text); const primaryOrigin = parseOriginUrl(urls[0]); originUrl = urls[0]; originScheme = primaryOrigin.scheme; originAddress = primaryOrigin.address; originPort = primaryOrigin.port; originUri = primaryOrigin.uri; upstreams = urls.slice(1); } else if (values.upstream_type === 'tunnel') { originUrl = `${values.tunnel_target_protocol}://${values.tunnel_target_addr}`; originScheme = values.tunnel_target_protocol as 'http' | 'https'; originAddress = values.tunnel_target_addr || ''; } else { originUrl = 'http://127.0.0.1'; originScheme = 'http'; originAddress = '127.0.0.1'; originPort = '80'; } const { headers } = parseCustomHeadersText( values.custom_headers_text, ); await save( { origin_id: null, origin_url: originUrl, origin_scheme: originScheme, origin_address: originAddress, origin_port: originPort, origin_uri: originUri, origin_host: values.origin_host.trim(), upstreams, custom_headers: headers, upstream_type: values.upstream_type, tunnel_node_id: values.upstream_type === 'tunnel' && values.tunnel_id ? Number(values.tunnel_id) : null, tunnel_target_addr: values.upstream_type === 'tunnel' ? values.tunnel_target_addr : '', tunnel_target_protocol: values.upstream_type === 'tunnel' ? values.tunnel_target_protocol : '', pages_project_id: values.upstream_type === 'pages' && values.pages_project_id ? Number(values.pages_project_id) : null, }, '反向代理设置已保存', ); })} > ( 回源方式
{( [ ['direct', '直连上游'], ['tunnel', '内网穿透 (Tunnel)'], ['pages', 'Pages 静态站点'], ] as const ).map(([value, label]) => ( ))}
)} /> {upstreamType === 'direct' ? ( ( 上游地址