fix(waf): UA 检查开启后才显示匹配与屏蔽并补充说明

未开启 require_ua 时隐藏匹配/屏蔽区块;爬虫与非正常 UA 开关增加分类提示。
This commit is contained in:
ryan
2026-07-19 11:38:18 +08:00
parent 28eef0bbcd
commit 7476c86976
2 changed files with 147 additions and 102 deletions
@@ -5,7 +5,7 @@ import type { WAFIPGroup, WAFRuleNode } from '@/lib/services/openflare';
import { NodeProperties } from './node-properties'; import { NodeProperties } from './node-properties';
it('toggles UA check switches and match mode', () => { it('hides match and block until UA check is enabled', () => {
const node: WAFRuleNode = { const node: WAFRuleNode = {
id: 'ua', id: 'ua',
type: 'ua_check', type: 'ua_check',
@@ -20,17 +20,31 @@ it('toggles UA check switches and match mode', () => {
}, },
}; };
const onChange = vi.fn(); const onChange = vi.fn();
render(<NodeProperties node={node} ipGroups={[]} onChange={onChange} />); const { rerender } = render(
<NodeProperties node={node} ipGroups={[]} onChange={onChange} />,
);
expect(screen.queryByLabelText('屏蔽常见爬虫 UA')).not.toBeInTheDocument();
expect(screen.queryByLabelText('浏览器')).not.toBeInTheDocument();
fireEvent.click(screen.getByLabelText('开启 UA 检查')); fireEvent.click(screen.getByLabelText('开启 UA 检查'));
expect(onChange).toHaveBeenCalledWith( expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
config: expect.objectContaining({ require_ua: true }), config: expect.objectContaining({ require_ua: true }),
}), }),
); );
rerender(
<NodeProperties
node={{ ...node, config: { ...node.config, require_ua: true } }}
ipGroups={[]}
onChange={onChange}
/>,
);
expect(screen.getByLabelText('屏蔽常见爬虫 UA')).toBeInTheDocument(); expect(screen.getByLabelText('屏蔽常见爬虫 UA')).toBeInTheDocument();
expect(screen.getByLabelText('屏蔽非正常 UA')).toBeInTheDocument(); expect(screen.getByLabelText('屏蔽非正常 UA')).toBeInTheDocument();
expect( expect(
screen.getByText('命中返回 false,优先级高于匹配'), screen.getByText(/浏览器或操作系统分类为 Bot/),
).toBeInTheDocument();
expect(
screen.getByText(/浏览器分类为 Bot、Other 或 Unknown/),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -154,7 +154,7 @@ function PropertyFields({
开启 UA 检查 开启 UA 检查
</FieldLabel> </FieldLabel>
<FieldDescription> <FieldDescription>
开启后如果请求头不携带 UA 返回 False 开启后如果请求头不携带 UA 返回 False;并可配置匹配与屏蔽
</FieldDescription> </FieldDescription>
</div> </div>
<Switch <Switch
@@ -166,104 +166,135 @@ function PropertyFields({
/> />
</Field> </Field>
</div> </div>
<Separator /> {node.config.require_ua && (
<div className='space-y-3'> <>
<p className='text-xs font-medium text-muted-foreground'>UA 匹配</p> <Separator />
<Field> <div className='space-y-3'>
<FieldLabel htmlFor={`${node.id}-match-mode`}>匹配模式</FieldLabel> <p className='text-xs font-medium text-muted-foreground'>
<Select UA 匹配
value={node.config.match_mode} </p>
onValueChange={(match_mode: 'and' | 'or') => <Field>
onChange({ ...node, config: { ...node.config, match_mode } }) <FieldLabel htmlFor={`${node.id}-match-mode`}>
} 匹配模式
> </FieldLabel>
<SelectTrigger id={`${node.id}-match-mode`} className='w-full'> <Select
<SelectValue /> value={node.config.match_mode}
</SelectTrigger> onValueChange={(match_mode: 'and' | 'or') =>
<SelectContent> onChange({
<SelectGroup> ...node,
<SelectItem value='or'>或(OR)</SelectItem> config: { ...node.config, match_mode },
<SelectItem value='and'>且(AND)</SelectItem> })
</SelectGroup> }
</SelectContent> >
</Select> <SelectTrigger
<FieldDescription> id={`${node.id}-match-mode`}
浏览器与操作系统两侧都有选择时生效 className='w-full'
</FieldDescription> >
</Field> <SelectValue />
<MultiSelect </SelectTrigger>
id={`${node.id}-browsers`} <SelectContent>
label='浏览器' <SelectGroup>
options={UA_BROWSER_OPTIONS.map((option) => ({ <SelectItem value='or'>或(OR)</SelectItem>
value: option.value, <SelectItem value='and'>且(AND)</SelectItem>
label: option.label, </SelectGroup>
searchText: `${option.label} ${option.value}`, </SelectContent>
}))} </Select>
value={node.config.browsers} <FieldDescription>
onChange={(browsers) => 浏览器与操作系统两侧都有选择时生效
onChange({ ...node, config: { ...node.config, browsers } }) </FieldDescription>
} </Field>
/> <MultiSelect
<MultiSelect id={`${node.id}-browsers`}
id={`${node.id}-os`} label='浏览器'
label='操作系统' options={UA_BROWSER_OPTIONS.map((option) => ({
options={UA_OS_OPTIONS.map((option) => ({ value: option.value,
value: option.value, label: option.label,
label: option.label, searchText: `${option.label} ${option.value}`,
searchText: `${option.label} ${option.value}`, }))}
}))} value={node.config.browsers}
value={node.config.operating_systems} onChange={(browsers) =>
onChange={(operating_systems) => onChange({ ...node, config: { ...node.config, browsers } })
onChange({ }
...node, />
config: { ...node.config, operating_systems }, <MultiSelect
}) id={`${node.id}-os`}
} label='操作系统'
/> options={UA_OS_OPTIONS.map((option) => ({
</div> value: option.value,
<Separator /> label: option.label,
<div className='space-y-3'> searchText: `${option.label} ${option.value}`,
<div className='space-y-1'> }))}
<p className='text-xs font-medium text-muted-foreground'>屏蔽</p> value={node.config.operating_systems}
<FieldDescription>命中返回 false,优先级高于匹配</FieldDescription> onChange={(operating_systems) =>
</div> onChange({
<Field ...node,
orientation='horizontal' config: { ...node.config, operating_systems },
className='items-center justify-between' })
> }
<FieldLabel htmlFor={`${node.id}-block-bots`}> />
屏蔽常见爬虫 UA </div>
</FieldLabel> <Separator />
<Switch <div className='space-y-3'>
id={`${node.id}-block-bots`} <div className='space-y-1'>
checked={node.config.block_common_bots} <p className='text-xs font-medium text-muted-foreground'>
onCheckedChange={(block_common_bots) => 屏蔽
onChange({ </p>
...node, <FieldDescription>
config: { ...node.config, block_common_bots }, 命中返回 false,优先级高于匹配
}) </FieldDescription>
} </div>
/> <Field
</Field> orientation='horizontal'
<Field className='items-start justify-between gap-3'
orientation='horizontal' >
className='items-center justify-between' <div className='space-y-1'>
> <FieldLabel htmlFor={`${node.id}-block-bots`}>
<FieldLabel htmlFor={`${node.id}-block-abnormal`}> 屏蔽常见爬虫 UA
屏蔽非正常 UA </FieldLabel>
</FieldLabel> <FieldDescription>
<Switch 浏览器或操作系统分类为 Bot(含 bot / spider / crawler /
id={`${node.id}-block-abnormal`} slurp 等特征,如 Googlebot)
checked={node.config.block_abnormal_ua} </FieldDescription>
onCheckedChange={(block_abnormal_ua) => </div>
onChange({ <Switch
...node, id={`${node.id}-block-bots`}
config: { ...node.config, block_abnormal_ua }, className='mt-0.5'
}) checked={node.config.block_common_bots}
} onCheckedChange={(block_common_bots) =>
/> onChange({
</Field> ...node,
</div> config: { ...node.config, block_common_bots },
})
}
/>
</Field>
<Field
orientation='horizontal'
className='items-start justify-between gap-3'
>
<div className='space-y-1'>
<FieldLabel htmlFor={`${node.id}-block-abnormal`}>
屏蔽非正常 UA
</FieldLabel>
<FieldDescription>
浏览器分类为 Bot、Other 或 Unknown(无法识别为已知浏览器)
</FieldDescription>
</div>
<Switch
id={`${node.id}-block-abnormal`}
className='mt-0.5'
checked={node.config.block_abnormal_ua}
onCheckedChange={(block_abnormal_ua) =>
onChange({
...node,
config: { ...node.config, block_abnormal_ua },
})
}
/>
</Field>
</div>
</>
)}
</FieldGroup> </FieldGroup>
); );
if (node.type === 'pow') if (node.type === 'pow')