'use client'; import { useEffect } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { Plus } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; import type { WAFIPGroup, WAFIPGroupPayload, WAFIPGroupSubscriptionFormat, WAFIPGroupType, } from '@/lib/services/openflare'; import { automaticPresetRules, listToText, parseAutomaticConfig, parseTextareaList, } from './helpers'; const ipGroupSchema = z .object({ name: z.string().trim().min(1, '请输入 IP 组名称').max(255), type: z.enum(['manual', 'automatic', 'subscription']), enabled: z.boolean(), ip_list_text: z.string(), auto_config_text: z.string(), subscription_url: z.string(), subscription_format: z.enum(['text', 'json']), subscription_mapping_rule: z.string(), sync_interval_minutes: z.number().int().min(5), }) .superRefine((value, context) => { if (value.type === 'subscription' && !value.subscription_url.trim()) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['subscription_url'], message: '订阅类型需要填写订阅 URL', }); } if (value.type === 'automatic') { try { parseAutomaticConfig(value.auto_config_text); } catch (error) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['auto_config_text'], message: error instanceof Error ? error.message : '自动配置格式错误', }); } } }); type IPGroupFormValues = z.infer; const defaultValues: IPGroupFormValues = { name: '', type: 'manual', enabled: true, ip_list_text: '', auto_config_text: '{}', subscription_url: '', subscription_format: 'text', subscription_mapping_rule: '', sync_interval_minutes: 1440, }; function buildFormValues(group: WAFIPGroup | null): IPGroupFormValues { if (!group) return defaultValues; return { name: group.name, type: group.type, enabled: group.enabled, ip_list_text: listToText(group.ip_list), auto_config_text: JSON.stringify(group.auto_config ?? {}, null, 2), subscription_url: group.subscription_url ?? '', subscription_format: group.subscription_format ?? 'text', subscription_mapping_rule: group.subscription_mapping_rule ?? '', sync_interval_minutes: group.sync_interval_minutes || 1440, }; } function buildPayload(values: IPGroupFormValues): WAFIPGroupPayload { const autoConfig = values.type === 'automatic' ? parseAutomaticConfig(values.auto_config_text) : {}; return { name: values.name.trim(), type: values.type, enabled: values.enabled, ip_list: parseTextareaList(values.ip_list_text), auto_config: autoConfig, subscription_url: values.subscription_url.trim(), subscription_format: values.subscription_format, subscription_mapping_rule: values.subscription_mapping_rule.trim(), sync_interval_minutes: values.sync_interval_minutes, }; } function appendAutomaticPresetRule( autoConfigText: string, rule: (typeof automaticPresetRules)[number], ) { const config = parseAutomaticConfig(autoConfigText); const rules = Array.isArray(config.rules) ? config.rules : []; const exists = rules.some( (item) => item && typeof item === 'object' && 'expr' in item && (item as { expr?: unknown }).expr === rule.expr, ); const nextRules = exists ? rules : [...rules, rule]; return JSON.stringify( { lookback_minutes: typeof config.lookback_minutes === 'number' ? config.lookback_minutes : 60, ...config, rules: nextRules, }, null, 2, ); } interface IPGroupDialogProps { open: boolean; group: WAFIPGroup | null; submitting: boolean; onOpenChange: (open: boolean) => void; onSubmit: (payload: WAFIPGroupPayload) => Promise; } export function IPGroupDialog({ open, group, submitting, onOpenChange, onSubmit, }: IPGroupDialogProps) { const form = useForm({ resolver: zodResolver(ipGroupSchema), defaultValues, }); const type = form.watch('type'); useEffect(() => { if (!open) return; form.reset(buildFormValues(group)); }, [form, group, open]); const handleSubmit = form.handleSubmit(async (values) => { try { await onSubmit(buildPayload(values)); onOpenChange(false); } catch (error) { form.setError('root', { message: error instanceof Error ? error.message : '保存失败', }); } }); return ( {group ? `编辑 ${group.name}` : '新建 IP 组'} 维护可被 WAF IP 黑白名单引用的手动、自动与订阅 IP 集合。
( IP 组名称 )} /> ( 类型 )} /> (
启用 IP 组 关闭后保留配置,但发布时不会展开到 WAF 运行时名单。
)} />
{type === 'subscription' ? (
( 订阅 URL )} /> ( 订阅格式 )} /> ( 同步间隔(分钟) field.onChange(Number(event.target.value)) } /> 最小 5 分钟,默认 1440 分钟。 )} /> ( JSON 映射规则 )} />
) : null} {type === 'automatic' ? (
( 同步间隔(分钟) field.onChange(Number(event.target.value)) } /> 定时从请求日志挖掘恶意 IP 的周期。最小 5 分钟。 )} />
预设规则
{automaticPresetRules.map((rule) => ( ))}
( 自动配置 JSON