diff --git a/frontend/app/(main)/proxy-routes/detail/components/waf-section.tsx b/frontend/app/(main)/proxy-routes/detail/components/waf-section.tsx index 8c1cc5d0..96aac6f9 100644 --- a/frontend/app/(main)/proxy-routes/detail/components/waf-section.tsx +++ b/frontend/app/(main)/proxy-routes/detail/components/waf-section.tsx @@ -127,11 +127,7 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) { {group.name} {group.enabled ? '启用中' : '已停用'} ·{' '} - {group.ip_whitelist.length + - group.ip_blacklist.length + - group.country_whitelist.length + - group.country_blacklist.length}{' '} - 条规则 + {group.graph.nodes.length} 个节点 @@ -145,4 +141,4 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) { )} ); -} \ No newline at end of file +} diff --git a/frontend/app/(main)/waf/components/create-rule-dialog.tsx b/frontend/app/(main)/waf/components/create-rule-dialog.tsx new file mode 100644 index 00000000..1214e6e8 --- /dev/null +++ b/frontend/app/(main)/waf/components/create-rule-dialog.tsx @@ -0,0 +1,87 @@ +'use client'; + +import {useEffect, useState} from 'react'; + +import {Button} from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import {Field, FieldGroup, FieldLabel} from '@/components/ui/field'; +import {Input} from '@/components/ui/input'; +import {Spinner} from '@/components/ui/spinner'; + +interface CreateRuleDialogProps { + open: boolean; + pending: boolean; + onOpenChange: (open: boolean) => void; + onCreate: (name: string) => Promise; +} + +export function CreateRuleDialog({ + open, + pending, + onOpenChange, + onCreate, +}: CreateRuleDialogProps) { + const [name, setName] = useState(''); + + useEffect(() => { + if (!open) setName(''); + }, [open]); + + const trimmedName = name.trim(); + + return ( + + +
{ + event.preventDefault(); + if (!trimmedName || pending) return; + await onCreate(trimmedName); + }} + > + + 新建 WAF 规则 + + 创建后将进入编排页面,通过处理单元配置规则执行流程。 + + + + + 规则名称 + setName(event.target.value)} + placeholder="例如:入口防护" + autoComplete="off" + autoFocus + /> + + + + + + +
+
+
+ ); +} diff --git a/frontend/app/(main)/waf/components/rule-group-sheet.tsx b/frontend/app/(main)/waf/components/rule-group-sheet.tsx deleted file mode 100644 index e7ea2e56..00000000 --- a/frontend/app/(main)/waf/components/rule-group-sheet.tsx +++ /dev/null @@ -1,375 +0,0 @@ -'use client'; - -import {useEffect, useMemo, useState} 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 {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from '@/components/ui/form'; -import {Input} from '@/components/ui/input'; -import {Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle,} from '@/components/ui/sheet'; -import {Switch} from '@/components/ui/switch'; -import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'; -import {Textarea} from '@/components/ui/textarea'; -import type {WAFIPGroup, WAFRuleGroup, WAFRuleGroupPayload} from '@/lib/services/openflare'; - -import { - buildCountryOptions, - buildRuleGroupDraft, - countRuleEntries, - emptyRuleGroupDraft, - formatCountryItem, - getListFieldKey, - type ListFieldKey, - normalizeItems, - textToList, - updateDraftList, -} from './helpers'; -import {PowConfigPanel} from './pow-config-panel'; -import {RuleEntryDialog, type RuleEntryFormValues} from './rule-entry-dialog'; -import {RuleListSection} from './rule-list-section'; - -const ruleGroupSchema = z.object({ - name: z.string().trim().min(1, '请输入规则组名称').max(255, '名称不能超过 255 个字符'), - enabled: z.boolean(), - block_status_code: z.number().int().min(400).max(599), - block_response_body: z.string(), - pow_enabled: z.boolean(), -}); - -type RuleGroupFormValues = z.infer; - -interface RuleGroupSheetProps { - open: boolean; - group: WAFRuleGroup | null; - ipGroups: WAFIPGroup[]; - submitting: boolean; - onOpenChange: (open: boolean) => void; - onSubmit: (payload: WAFRuleGroupPayload) => Promise; -} - -export function RuleGroupSheet({ - open, - group, - ipGroups, - submitting, - onOpenChange, - onSubmit, -}: RuleGroupSheetProps) { - const [listsDraft, setListsDraft] = useState(emptyRuleGroupDraft); - const [ruleEntryOpen, setRuleEntryOpen] = useState(false); - - const countryOptions = useMemo(() => buildCountryOptions(), []); - const countryLabelMap = useMemo( - () => new Map(countryOptions.map((option) => [option.code, option.label])), - [countryOptions], - ); - - const form = useForm({ - resolver: zodResolver(ruleGroupSchema), - defaultValues: { - name: '', - enabled: true, - block_status_code: 418, - block_response_body: '', - pow_enabled: false, - }, - }); - - useEffect(() => { - if (!open) return; - const draft = buildRuleGroupDraft(group); - setListsDraft(draft); - form.reset({ - name: draft.name, - enabled: draft.enabled, - block_status_code: draft.block_status_code, - block_response_body: draft.block_response_body, - pow_enabled: draft.pow_enabled, - }); - setRuleEntryOpen(false); - }, [form, group, open]); - - const ipGroupByID = new Map(ipGroups.map((item) => [item.id, item])); - const whitelistGroupItems = listsDraft.ip_whitelist_group_ids - .map((id) => ipGroupByID.get(id)) - .filter((item): item is WAFIPGroup => Boolean(item)); - const blacklistGroupItems = listsDraft.ip_blacklist_group_ids - .map((id) => ipGroupByID.get(id)) - .filter((item): item is WAFIPGroup => Boolean(item)); - - const removeRuleItem = (key: ListFieldKey, value: string) => { - setListsDraft((current) => - updateDraftList(current, key, (items) => items.filter((item) => item !== value)), - ); - }; - - const removeRuleGroup = (key: ListFieldKey, id: number) => { - setListsDraft((current) => - updateDraftList(current, key, (items) => - items.filter((item) => item !== String(id)), - ), - ); - }; - - const applyRuleEntry = (entry: RuleEntryFormValues) => { - const values = - entry.dimension === 'ip' - ? textToList(entry.ipValue) - : entry.dimension === 'ip_group' - ? entry.ipGroupIDs.map(String) - : normalizeItems(entry.countryValues); - - const listKey = getListFieldKey(entry.listType, entry.dimension); - setListsDraft((current) => - updateDraftList(current, listKey, (items) => - normalizeItems([...items, ...values]), - ), - ); - setRuleEntryOpen(false); - }; - - const handleSubmit = form.handleSubmit(async (values) => { - const payload: WAFRuleGroupPayload = { - ...listsDraft, - name: values.name, - enabled: values.enabled, - block_status_code: values.block_status_code, - block_response_body: values.block_response_body, - pow_enabled: values.pow_enabled, - }; - try { - await onSubmit(payload); - onOpenChange(false); - } catch (error) { - form.setError('root', { - message: error instanceof Error ? error.message : '保存失败', - }); - } - }); - - const isGlobal = group?.is_global ?? false; - - return ( - <> - - - - {group ? `编辑 ${group.name}` : '新建规则组'} - - 白名单命中后直接放行;未命中白名单时继续判断黑名单。当前规则数{' '} - {countRuleEntries(listsDraft)} 条。 - - - -
- -
- - - 基本信息 - 黑白名单 - PoW - 拦截返回 - - - -
- ( - -
- 启用规则组 - 关闭后保留配置,但不会参与匹配。 -
- - - -
- )} - /> - ( - - 规则组名称 - - - - - - )} - /> -
-
- - -
-

- 通过弹窗添加 IP、IP 组或地域规则。 -

- -
-
- removeRuleItem('ip_whitelist', item)} - onRemoveGroup={(id) => removeRuleGroup('ip_whitelist_group_ids', id)} - /> - removeRuleItem('ip_blacklist', item)} - onRemoveGroup={(id) => removeRuleGroup('ip_blacklist_group_ids', id)} - /> - - formatCountryItem(code, countryLabelMap), - )} - tone="whitelist" - emptyText="暂无地域白名单规则。" - onRemove={(item) => { - const code = item.split(' ')[0] ?? item; - removeRuleItem('country_whitelist', code); - }} - /> - - formatCountryItem(code, countryLabelMap), - )} - tone="blacklist" - emptyText="暂无地域黑名单规则。" - onRemove={(item) => { - const code = item.split(' ')[0] ?? item; - removeRuleItem('country_blacklist', code); - }} - /> -
-
- - - { - form.setValue('pow_enabled', enabled); - setListsDraft((current) => ({ - ...current, - pow_enabled: enabled, - pow_config: config, - })); - }} - /> - - - -
- ( - - 拦截状态码 - - - field.onChange(Number(event.target.value)) - } - /> - -
- {[403, 418, 451, 503].map((code) => ( - - ))} -
- -
- )} - /> - ( - - 拦截页面 - -