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';
it('toggles UA check switches and match mode', () => {
it('hides match and block until UA check is enabled', () => {
const node: WAFRuleNode = {
id: 'ua',
type: 'ua_check',
@@ -20,17 +20,31 @@ it('toggles UA check switches and match mode', () => {
},
};
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 检查'));
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
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.getByText('命中返回 false,优先级高于匹配'),
screen.getByText(/浏览器或操作系统分类为 Bot/),
).toBeInTheDocument();
expect(
screen.getByText(/浏览器分类为 Bot、Other 或 Unknown/),
).toBeInTheDocument();
});
@@ -154,7 +154,7 @@ function PropertyFields({
开启 UA 检查
</FieldLabel>
<FieldDescription>
开启后如果请求头不携带 UA 返回 False
开启后如果请求头不携带 UA 返回 False;并可配置匹配与屏蔽
</FieldDescription>
</div>
<Switch
@@ -166,104 +166,135 @@ function PropertyFields({
/>
</Field>
</div>
<Separator />
<div className='space-y-3'>
<p className='text-xs font-medium text-muted-foreground'>UA 匹配</p>
<Field>
<FieldLabel htmlFor={`${node.id}-match-mode`}>匹配模式</FieldLabel>
<Select
value={node.config.match_mode}
onValueChange={(match_mode: 'and' | 'or') =>
onChange({ ...node, config: { ...node.config, match_mode } })
}
>
<SelectTrigger id={`${node.id}-match-mode`} className='w-full'>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value='or'>或(OR)</SelectItem>
<SelectItem value='and'>且(AND)</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FieldDescription>
浏览器与操作系统两侧都有选择时生效
</FieldDescription>
</Field>
<MultiSelect
id={`${node.id}-browsers`}
label='浏览器'
options={UA_BROWSER_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
searchText: `${option.label} ${option.value}`,
}))}
value={node.config.browsers}
onChange={(browsers) =>
onChange({ ...node, config: { ...node.config, browsers } })
}
/>
<MultiSelect
id={`${node.id}-os`}
label='操作系统'
options={UA_OS_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
searchText: `${option.label} ${option.value}`,
}))}
value={node.config.operating_systems}
onChange={(operating_systems) =>
onChange({
...node,
config: { ...node.config, operating_systems },
})
}
/>
</div>
<Separator />
<div className='space-y-3'>
<div className='space-y-1'>
<p className='text-xs font-medium text-muted-foreground'>屏蔽</p>
<FieldDescription>命中返回 false,优先级高于匹配</FieldDescription>
</div>
<Field
orientation='horizontal'
className='items-center justify-between'
>
<FieldLabel htmlFor={`${node.id}-block-bots`}>
屏蔽常见爬虫 UA
</FieldLabel>
<Switch
id={`${node.id}-block-bots`}
checked={node.config.block_common_bots}
onCheckedChange={(block_common_bots) =>
onChange({
...node,
config: { ...node.config, block_common_bots },
})
}
/>
</Field>
<Field
orientation='horizontal'
className='items-center justify-between'
>
<FieldLabel htmlFor={`${node.id}-block-abnormal`}>
屏蔽非正常 UA
</FieldLabel>
<Switch
id={`${node.id}-block-abnormal`}
checked={node.config.block_abnormal_ua}
onCheckedChange={(block_abnormal_ua) =>
onChange({
...node,
config: { ...node.config, block_abnormal_ua },
})
}
/>
</Field>
</div>
{node.config.require_ua && (
<>
<Separator />
<div className='space-y-3'>
<p className='text-xs font-medium text-muted-foreground'>
UA 匹配
</p>
<Field>
<FieldLabel htmlFor={`${node.id}-match-mode`}>
匹配模式
</FieldLabel>
<Select
value={node.config.match_mode}
onValueChange={(match_mode: 'and' | 'or') =>
onChange({
...node,
config: { ...node.config, match_mode },
})
}
>
<SelectTrigger
id={`${node.id}-match-mode`}
className='w-full'
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value='or'>或(OR)</SelectItem>
<SelectItem value='and'>且(AND)</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FieldDescription>
浏览器与操作系统两侧都有选择时生效
</FieldDescription>
</Field>
<MultiSelect
id={`${node.id}-browsers`}
label='浏览器'
options={UA_BROWSER_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
searchText: `${option.label} ${option.value}`,
}))}
value={node.config.browsers}
onChange={(browsers) =>
onChange({ ...node, config: { ...node.config, browsers } })
}
/>
<MultiSelect
id={`${node.id}-os`}
label='操作系统'
options={UA_OS_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
searchText: `${option.label} ${option.value}`,
}))}
value={node.config.operating_systems}
onChange={(operating_systems) =>
onChange({
...node,
config: { ...node.config, operating_systems },
})
}
/>
</div>
<Separator />
<div className='space-y-3'>
<div className='space-y-1'>
<p className='text-xs font-medium text-muted-foreground'>
屏蔽
</p>
<FieldDescription>
命中返回 false,优先级高于匹配
</FieldDescription>
</div>
<Field
orientation='horizontal'
className='items-start justify-between gap-3'
>
<div className='space-y-1'>
<FieldLabel htmlFor={`${node.id}-block-bots`}>
屏蔽常见爬虫 UA
</FieldLabel>
<FieldDescription>
浏览器或操作系统分类为 Bot(含 bot / spider / crawler /
slurp 等特征,如 Googlebot)
</FieldDescription>
</div>
<Switch
id={`${node.id}-block-bots`}
className='mt-0.5'
checked={node.config.block_common_bots}
onCheckedChange={(block_common_bots) =>
onChange({
...node,
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>
);
if (node.type === 'pow')