mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 11:46:16 +08:00
feat(settings): add global blacklist management page and UI integration
- Add new global blacklist settings page with pattern management interface - Create useGlobalBlacklist and useUpdateGlobalBlacklist React Query hooks for data fetching and mutations - Implement global-blacklist.service.ts with API integration for blacklist operations - Add Global Blacklist navigation item to app sidebar with Ban icon - Add internationalization support for blacklist UI with English and Chinese translations - Include pattern matching rules documentation (domain wildcards, keywords, IP addresses, CIDR ranges) - Add loading states, error handling, and success/error toast notifications - Implement textarea input with change tracking and save button state management
This commit is contained in:
132
frontend/app/[locale]/settings/blacklist/page.tsx
Normal file
132
frontend/app/[locale]/settings/blacklist/page.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
"use client"
|
||||
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { AlertTriangle, Loader2, Ban } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useGlobalBlacklist, useUpdateGlobalBlacklist } from "@/hooks/use-global-blacklist"
|
||||
|
||||
/**
|
||||
* Global blacklist settings page
|
||||
*/
|
||||
export default function GlobalBlacklistPage() {
|
||||
const t = useTranslations("pages.settings.blacklist")
|
||||
|
||||
const [blacklistText, setBlacklistText] = useState("")
|
||||
const [hasChanges, setHasChanges] = useState(false)
|
||||
|
||||
const { data, isLoading, error } = useGlobalBlacklist()
|
||||
const updateBlacklist = useUpdateGlobalBlacklist()
|
||||
|
||||
// Initialize text when data loads
|
||||
useEffect(() => {
|
||||
if (data?.patterns) {
|
||||
setBlacklistText(data.patterns.join("\n"))
|
||||
setHasChanges(false)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
// Handle text change
|
||||
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setBlacklistText(e.target.value)
|
||||
setHasChanges(true)
|
||||
}
|
||||
|
||||
// Handle save
|
||||
const handleSave = () => {
|
||||
const patterns = blacklistText
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
|
||||
updateBlacklist.mutate(
|
||||
{ patterns },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setHasChanges(false)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-4 w-96" />
|
||||
</div>
|
||||
<Skeleton className="h-[400px] w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center justify-center py-12">
|
||||
<AlertTriangle className="h-10 w-10 text-destructive mb-4" />
|
||||
<p className="text-muted-foreground">{t("loadError")}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
||||
{/* Page header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
||||
<p className="text-muted-foreground">{t("description")}</p>
|
||||
</div>
|
||||
|
||||
{/* Blacklist card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Ban className="h-5 w-5 text-muted-foreground" />
|
||||
<CardTitle>{t("card.title")}</CardTitle>
|
||||
</div>
|
||||
<CardDescription>{t("card.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Rules hint */}
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">{t("rules.title")}:</span>
|
||||
<span><code className="bg-muted px-1.5 py-0.5 rounded text-xs">*.gov</code> {t("rules.domain")}</span>
|
||||
<span><code className="bg-muted px-1.5 py-0.5 rounded text-xs">*cdn*</code> {t("rules.keyword")}</span>
|
||||
<span><code className="bg-muted px-1.5 py-0.5 rounded text-xs">192.168.1.1</code> {t("rules.ip")}</span>
|
||||
<span><code className="bg-muted px-1.5 py-0.5 rounded text-xs">10.0.0.0/8</code> {t("rules.cidr")}</span>
|
||||
</div>
|
||||
|
||||
{/* Scope hint */}
|
||||
<div className="rounded-lg border bg-muted/50 p-3 text-sm">
|
||||
<p className="text-muted-foreground">{t("scopeHint")}</p>
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<Textarea
|
||||
value={blacklistText}
|
||||
onChange={handleTextChange}
|
||||
placeholder={t("placeholder")}
|
||||
className="min-h-[320px] font-mono text-sm"
|
||||
/>
|
||||
|
||||
{/* Save button */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || updateBlacklist.isPending}
|
||||
>
|
||||
{updateBlacklist.isPending && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
IconMessageReport, // Feedback icon
|
||||
IconSearch, // Search icon
|
||||
IconKey, // API Key icon
|
||||
IconBan, // Blacklist icon
|
||||
} from "@tabler/icons-react"
|
||||
// Import internationalization hook
|
||||
import { useTranslations } from 'next-intl'
|
||||
@@ -174,6 +175,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
url: "/settings/api-keys/",
|
||||
icon: IconKey,
|
||||
},
|
||||
{
|
||||
name: t('globalBlacklist'),
|
||||
url: "/settings/blacklist/",
|
||||
icon: IconBan,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
40
frontend/hooks/use-global-blacklist.ts
Normal file
40
frontend/hooks/use-global-blacklist.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import {
|
||||
getGlobalBlacklist,
|
||||
updateGlobalBlacklist,
|
||||
type GlobalBlacklistResponse,
|
||||
type UpdateGlobalBlacklistRequest,
|
||||
} from '@/services/global-blacklist.service'
|
||||
|
||||
const QUERY_KEY = ['global-blacklist']
|
||||
|
||||
/**
|
||||
* Hook to fetch global blacklist
|
||||
*/
|
||||
export function useGlobalBlacklist() {
|
||||
return useQuery<GlobalBlacklistResponse>({
|
||||
queryKey: QUERY_KEY,
|
||||
queryFn: getGlobalBlacklist,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to update global blacklist
|
||||
*/
|
||||
export function useUpdateGlobalBlacklist() {
|
||||
const queryClient = useQueryClient()
|
||||
const t = useTranslations('pages.settings.blacklist')
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdateGlobalBlacklistRequest) => updateGlobalBlacklist(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: QUERY_KEY })
|
||||
toast.success(t('toast.saveSuccess'))
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('toast.saveError'))
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -320,6 +320,7 @@
|
||||
"systemLogs": "System Logs",
|
||||
"notifications": "Notifications",
|
||||
"apiKeys": "API Keys",
|
||||
"globalBlacklist": "Global Blacklist",
|
||||
"help": "Get Help",
|
||||
"feedback": "Feedback"
|
||||
},
|
||||
@@ -2053,7 +2054,7 @@
|
||||
"cidr": "Matches IP range",
|
||||
"cidrShort": "CIDR"
|
||||
},
|
||||
"placeholder": "Enter rules, one per line",
|
||||
"placeholder": "Enter rules, one per line\n\nExamples:\n*.gov\n*.edu\n*cdn*\n192.168.0.0/16\n10.0.0.1",
|
||||
"save": "Save Rules"
|
||||
},
|
||||
"scheduledScans": {
|
||||
@@ -2137,6 +2138,31 @@
|
||||
"nav": {
|
||||
"scanEngine": "Scan Engine",
|
||||
"wordlists": "Wordlist Management"
|
||||
},
|
||||
"settings": {
|
||||
"blacklist": {
|
||||
"title": "Global Blacklist",
|
||||
"description": "Configure global blacklist rules. Matching assets will be automatically excluded during scans.",
|
||||
"loadError": "Failed to load blacklist rules",
|
||||
"card": {
|
||||
"title": "Blacklist Rules",
|
||||
"description": "These rules apply to all target scans. To configure blacklist for a specific target, go to the target settings page."
|
||||
},
|
||||
"rules": {
|
||||
"title": "Supported rule types",
|
||||
"domain": "Domain",
|
||||
"keyword": "Keyword",
|
||||
"ip": "IP",
|
||||
"cidr": "CIDR"
|
||||
},
|
||||
"scopeHint": "Global rules apply to all targets. Target-level rules can be configured in Target → Settings.",
|
||||
"placeholder": "Enter rules, one per line\n\nExamples:\n*.gov\n*.edu\n*cdn*\n192.168.0.0/16\n10.0.0.1",
|
||||
"save": "Save Rules",
|
||||
"toast": {
|
||||
"saveSuccess": "Blacklist rules saved",
|
||||
"saveError": "Failed to save blacklist rules"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
|
||||
@@ -320,6 +320,7 @@
|
||||
"systemLogs": "系统日志",
|
||||
"notifications": "通知设置",
|
||||
"apiKeys": "API 密钥",
|
||||
"globalBlacklist": "全局黑名单",
|
||||
"help": "获取帮助",
|
||||
"feedback": "反馈建议"
|
||||
},
|
||||
@@ -2053,7 +2054,7 @@
|
||||
"cidr": "匹配 IP 网段范围",
|
||||
"cidrShort": "CIDR"
|
||||
},
|
||||
"placeholder": "输入规则,每行一个",
|
||||
"placeholder": "输入规则,每行一个\n\n示例:\n*.gov\n*.edu\n*cdn*\n192.168.0.0/16\n10.0.0.1",
|
||||
"save": "保存规则"
|
||||
},
|
||||
"scheduledScans": {
|
||||
@@ -2137,6 +2138,31 @@
|
||||
"nav": {
|
||||
"scanEngine": "扫描引擎",
|
||||
"wordlists": "字典管理"
|
||||
},
|
||||
"settings": {
|
||||
"blacklist": {
|
||||
"title": "全局黑名单",
|
||||
"description": "配置全局黑名单规则,扫描时将自动排除匹配的资产",
|
||||
"loadError": "加载黑名单规则失败",
|
||||
"card": {
|
||||
"title": "黑名单规则",
|
||||
"description": "这些规则将应用于所有目标的扫描任务。如需为特定目标配置黑名单,请前往目标设置页面。"
|
||||
},
|
||||
"rules": {
|
||||
"title": "支持的规则类型",
|
||||
"domain": "域名",
|
||||
"keyword": "关键词",
|
||||
"ip": "IP",
|
||||
"cidr": "CIDR"
|
||||
},
|
||||
"scopeHint": "全局规则对所有目标生效。目标级规则可在「目标 → 设置」中单独配置。",
|
||||
"placeholder": "输入规则,每行一个\n\n示例:\n*.gov\n*.edu\n*cdn*\n192.168.0.0/16\n10.0.0.1",
|
||||
"save": "保存规则",
|
||||
"toast": {
|
||||
"saveSuccess": "黑名单规则已保存",
|
||||
"saveError": "保存黑名单规则失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
|
||||
25
frontend/services/global-blacklist.service.ts
Normal file
25
frontend/services/global-blacklist.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
|
||||
export interface GlobalBlacklistResponse {
|
||||
patterns: string[]
|
||||
}
|
||||
|
||||
export interface UpdateGlobalBlacklistRequest {
|
||||
patterns: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global blacklist rules
|
||||
*/
|
||||
export async function getGlobalBlacklist(): Promise<GlobalBlacklistResponse> {
|
||||
const res = await api.get<GlobalBlacklistResponse>('/blacklist/rules/')
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Update global blacklist rules (full replace)
|
||||
*/
|
||||
export async function updateGlobalBlacklist(data: UpdateGlobalBlacklistRequest): Promise<GlobalBlacklistResponse> {
|
||||
const res = await api.put<GlobalBlacklistResponse>('/blacklist/rules/', data)
|
||||
return res.data
|
||||
}
|
||||
Reference in New Issue
Block a user