feat: support web
This commit is contained in:
16
assets/index.html
Normal file
16
assets/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>VulnFeed - Vulnerability Management</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
26
assets/package.json
Normal file
26
assets/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "vulnfeed-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"axios": "^1.3.4",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"postcss": "^8.4.21",
|
||||
"vite": "^4.1.4",
|
||||
"@vitejs/plugin-react": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11"
|
||||
}
|
||||
}
|
||||
1589
assets/pnpm-lock.yaml
generated
Normal file
1589
assets/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
assets/postcss.config.js
Normal file
6
assets/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
54
assets/src/App.jsx
Normal file
54
assets/src/App.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import LoginPage from './routes/auth/login'
|
||||
import HomePage from './routes/home'
|
||||
import VulnerabilityListPage from './routes/vuln/list'
|
||||
import VulnerabilityDetailPage from './routes/vuln/detail'
|
||||
import SyncDataTaskPage from './routes/sync/task'
|
||||
import ProtectedRoute from './components/ProtectedRoute'
|
||||
import Layout from './components/Layout'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Layout>
|
||||
<VulnerabilityListPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/vulns"
|
||||
element={
|
||||
<Layout>
|
||||
<VulnerabilityListPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/vulns/:id"
|
||||
element={
|
||||
<Layout>
|
||||
<VulnerabilityDetailPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/sync/task"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<SyncDataTaskPage />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
25
assets/src/components/Layout.jsx
Normal file
25
assets/src/components/Layout.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import Navigation from './Navigation'
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-gray-50">
|
||||
<Navigation />
|
||||
<div className="flex-grow py-10">
|
||||
<main>
|
||||
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<footer className="bg-white border-t border-gray-200">
|
||||
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
© {new Date().getFullYear()} VulnFeed. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
120
assets/src/components/Navigation.jsx
Normal file
120
assets/src/components/Navigation.jsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
|
||||
const Navigation = () => {
|
||||
const location = useLocation()
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false)
|
||||
const userMenuRef = useRef(null)
|
||||
|
||||
// 检查用户是否已登录
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
setIsAuthenticated(!!token)
|
||||
}, [])
|
||||
|
||||
const isActive = (path) => {
|
||||
return location.pathname === path
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
// 清除token
|
||||
localStorage.removeItem('token')
|
||||
// 重定向到登录页面
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
// 点击外部关闭用户菜单
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (userMenuRef.current && !userMenuRef.current.contains(event.target)) {
|
||||
setUserMenuOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<nav className="bg-white shadow">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<span className="text-xl font-bold text-indigo-600">VulnFeed</span>
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
<Link
|
||||
to="/vulns"
|
||||
className={`${
|
||||
isActive('/vulns') || location.pathname === '/'
|
||||
? 'border-indigo-500 text-gray-900'
|
||||
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
|
||||
} inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium`}
|
||||
>
|
||||
漏洞列表
|
||||
</Link>
|
||||
{isAuthenticated && (
|
||||
<Link
|
||||
to="/sync/task"
|
||||
className={`${
|
||||
isActive('/sync/task')
|
||||
? 'border-indigo-500 text-gray-900'
|
||||
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
|
||||
} inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium`}
|
||||
>
|
||||
同步任务
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isAuthenticated ? (
|
||||
<div className="hidden sm:ml-6 sm:flex sm:items-center">
|
||||
<div className="ml-3 relative">
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
||||
className="flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<span className="sr-only">打开用户菜单</span>
|
||||
<div className="h-8 w-8 rounded-full bg-indigo-100 flex items-center justify-center">
|
||||
<span className="text-indigo-800 font-medium">U</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{userMenuOpen && (
|
||||
<div
|
||||
ref={userMenuRef}
|
||||
className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
登出
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden sm:ml-6 sm:flex sm:items-center">
|
||||
<Link
|
||||
to="/login"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
登录
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navigation
|
||||
36
assets/src/components/ProtectedRoute.jsx
Normal file
36
assets/src/components/ProtectedRoute.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, Navigate } from 'react-router-dom'
|
||||
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
// 检查用户是否已登录
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
// 如果没有token,重定向到登录页面
|
||||
setIsAuthenticated(false)
|
||||
} else {
|
||||
// 如果有token,设置为已认证
|
||||
setIsAuthenticated(true)
|
||||
}
|
||||
}, [navigate])
|
||||
|
||||
// 如果还在检查认证状态,显示加载状态
|
||||
if (isAuthenticated === null) {
|
||||
return <div className="flex justify-center items-center h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
// 如果未认证,重定向到登录页面
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />
|
||||
}
|
||||
|
||||
// 如果已认证,渲染子组件
|
||||
return children
|
||||
}
|
||||
|
||||
export default ProtectedRoute
|
||||
3
assets/src/index.css
Normal file
3
assets/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
76
assets/src/lib/api.js
Normal file
76
assets/src/lib/api.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: '/api', // 使用相对路径,假设前端和后端在同一域名下
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// 在发送请求之前做些什么
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
// 对请求错误做些什么
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
// 对响应数据做些什么
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
// 对响应错误做些什么
|
||||
if (error.response?.status === 401) {
|
||||
// 如果是未授权错误,清除token并重定向到登录页面
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
// 登录API
|
||||
export const login = (username, password) => {
|
||||
return api.post('/login', { username, password })
|
||||
}
|
||||
|
||||
// 获取漏洞列表
|
||||
export const getVulnerabilities = (pageNo, pageSize, searchTerm = '') => {
|
||||
return api.get('/vulns', {
|
||||
params: {
|
||||
page_no: pageNo,
|
||||
page_size: pageSize,
|
||||
search: searchTerm,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 获取漏洞详情
|
||||
export const getVulnerabilityDetail = (id) => {
|
||||
return api.get(`/vulns/${id}`)
|
||||
}
|
||||
|
||||
// 获取同步任务
|
||||
export const getSyncDataTask = () => {
|
||||
return api.get('/sync_data_task')
|
||||
}
|
||||
|
||||
// 创建或更新同步任务
|
||||
export const createOrUpdateSyncDataTask = (data) => {
|
||||
return api.post('/sync_data_task', data)
|
||||
}
|
||||
|
||||
export default api
|
||||
13
assets/src/main.jsx
Normal file
13
assets/src/main.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
108
assets/src/routes/auth/login.jsx
Normal file
108
assets/src/routes/auth/login.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, Navigate } from 'react-router-dom'
|
||||
import { login } from '../../lib/api'
|
||||
|
||||
const LoginPage = () => {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
// 检查用户是否已经登录
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
return <Navigate to="/vulns" replace />
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const response = await login(username, password)
|
||||
const { token } = response.data.data
|
||||
|
||||
// 保存token到localStorage
|
||||
localStorage.setItem('token', token)
|
||||
|
||||
// 重定向到漏洞列表页面
|
||||
navigate('/vulns')
|
||||
} catch (err) {
|
||||
setError('登录失败,请检查用户名和密码')
|
||||
console.error('Login error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
VulnFeed 管理平台
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
登录到您的账户
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<input type="hidden" name="remember" defaultValue="true" />
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label htmlFor="username" className="sr-only">
|
||||
用户名
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="用户名"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
密码
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="密码"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-500 text-sm text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
||||
7
assets/src/routes/home.jsx
Normal file
7
assets/src/routes/home.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Navigate } from 'react-router-dom'
|
||||
|
||||
const HomePage = () => {
|
||||
return <Navigate to="/vulns" replace />
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
189
assets/src/routes/sync/task.jsx
Normal file
189
assets/src/routes/sync/task.jsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getSyncDataTask, createOrUpdateSyncDataTask } from '../../lib/api'
|
||||
|
||||
const SyncDataTaskPage = () => {
|
||||
const [task, setTask] = useState({
|
||||
name: '',
|
||||
interval_minutes: 60,
|
||||
status: true,
|
||||
})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchSyncDataTask()
|
||||
}, [])
|
||||
|
||||
const fetchSyncDataTask = async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const response = await getSyncDataTask()
|
||||
const { data } = response.data
|
||||
if (data) {
|
||||
setTask({
|
||||
name: data.name,
|
||||
interval_minutes: data.interval_minutes,
|
||||
status: data.status,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
setError('获取同步任务信息失败')
|
||||
console.error('Fetch sync data task error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value, type, checked } = e.target
|
||||
setTask(prev => ({
|
||||
...prev,
|
||||
[name]: type === 'checkbox' ? checked : type === 'number' ? parseInt(value) : value,
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setSaving(true)
|
||||
setError('')
|
||||
setSuccess('')
|
||||
|
||||
try {
|
||||
await createOrUpdateSyncDataTask(task)
|
||||
setSuccess('同步任务配置已保存')
|
||||
// 重新获取任务信息以确保数据是最新的
|
||||
fetchSyncDataTask()
|
||||
} catch (err) {
|
||||
setError('保存同步任务配置失败')
|
||||
console.error('Save sync data task error:', err)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-extrabold text-gray-900 mb-2">
|
||||
数据同步任务配置
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
配置和管理漏洞数据同步任务
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
|
||||
<div className="text-red-800 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-md p-4 mb-6">
|
||||
<div className="text-green-800 text-sm">
|
||||
{success}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<div className="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900">
|
||||
同步任务设置
|
||||
</h2>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="px-4 py-5 sm:px-6">
|
||||
<div className="grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
|
||||
<div className="sm:col-span-4">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
任务名称
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
value={task.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-4">
|
||||
<label htmlFor="interval_minutes" className="block text-sm font-medium text-gray-700">
|
||||
同步间隔(分钟)
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="number"
|
||||
name="interval_minutes"
|
||||
id="interval_minutes"
|
||||
min="1"
|
||||
max="1440"
|
||||
value={task.interval_minutes}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
设置数据同步的时间间隔,范围1-1440分钟(1天)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-4">
|
||||
<div className="flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="status"
|
||||
name="status"
|
||||
type="checkbox"
|
||||
checked={task.status}
|
||||
onChange={handleChange}
|
||||
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="status" className="font-medium text-gray-700">
|
||||
启用任务
|
||||
</label>
|
||||
<p className="text-gray-500">
|
||||
启用后将按照设定的时间间隔自动同步数据
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 border-t border-gray-200 pt-5">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
{saving ? '保存中...' : '保存配置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SyncDataTaskPage
|
||||
211
assets/src/routes/vuln/detail.jsx
Normal file
211
assets/src/routes/vuln/detail.jsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { getVulnerabilityDetail } from '../../lib/api'
|
||||
|
||||
const VulnerabilityDetailPage = () => {
|
||||
const { id } = useParams()
|
||||
const [vulnerability, setVulnerability] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchVulnerabilityDetail()
|
||||
}, [id])
|
||||
|
||||
const fetchVulnerabilityDetail = async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const response = await getVulnerabilityDetail(id)
|
||||
const { data } = response.data
|
||||
setVulnerability(data)
|
||||
} catch (err) {
|
||||
setError('获取漏洞详情失败')
|
||||
console.error('Fetch vulnerability detail error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
to="/vulns"
|
||||
className="inline-flex items-center text-indigo-600 hover:text-indigo-800"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
返回漏洞列表
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
|
||||
<div className="text-red-800 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
|
||||
</div>
|
||||
) : vulnerability ? (
|
||||
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<div className="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<h1 className="text-2xl leading-6 font-medium text-gray-900">
|
||||
{vulnerability.title}
|
||||
</h1>
|
||||
<div className="mt-2 flex items-center">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
||||
vulnerability.severity === 'Critical' ? 'bg-red-100 text-red-800' :
|
||||
vulnerability.severity === 'High' ? 'bg-orange-100 text-orange-800' :
|
||||
vulnerability.severity === 'Medium' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{vulnerability.severity}
|
||||
</span>
|
||||
<span className="ml-2 text-sm text-gray-500">
|
||||
CVE: {vulnerability.cve || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-5 sm:px-6">
|
||||
<div className="grid grid-cols-1 gap-y-4 gap-x-8 sm:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">基本信息</h3>
|
||||
<dl className="grid grid-cols-1 gap-y-2">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">漏洞编号</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">{vulnerability.key}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">披露日期</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
{vulnerability.disclosure ? formatDate(vulnerability.disclosure) : 'N/A'}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">来源</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">{vulnerability.source}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">标签</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{vulnerability.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">描述</h3>
|
||||
<p className="text-sm text-gray-900 whitespace-pre-wrap">
|
||||
{vulnerability.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">解决方案</h3>
|
||||
<p className="text-sm text-gray-900 whitespace-pre-wrap">
|
||||
{vulnerability.solutions || '暂无解决方案信息'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{vulnerability.reference_links && vulnerability.reference_links.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">参考链接</h3>
|
||||
<ul className="list-disc list-inside text-sm text-gray-900">
|
||||
{vulnerability.reference_links.map((link, index) => (
|
||||
<li key={index} className="mb-1">
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-600 hover:text-indigo-800"
|
||||
>
|
||||
{link}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{vulnerability.github_search && vulnerability.github_search.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">GitHub搜索</h3>
|
||||
<ul className="list-disc list-inside text-sm text-gray-900">
|
||||
{vulnerability.github_search.map((search, index) => (
|
||||
<li key={index} className="mb-1">
|
||||
{search}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">触发原因</h3>
|
||||
<ul className="list-disc list-inside text-sm text-gray-900">
|
||||
{vulnerability.reasons.map((reason, index) => (
|
||||
<li key={index} className="mb-1">
|
||||
{reason}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 gap-y-2 sm:grid-cols-2">
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">创建时间</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
{formatDate(vulnerability.created_at)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">更新时间</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
{formatDate(vulnerability.updated_at)}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<div className="px-4 py-5 sm:px-6">
|
||||
<div className="text-center text-gray-500">
|
||||
未找到漏洞信息
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VulnerabilityDetailPage
|
||||
196
assets/src/routes/vuln/list.jsx
Normal file
196
assets/src/routes/vuln/list.jsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { getVulnerabilities } from '../../lib/api'
|
||||
|
||||
const VulnerabilityListPage = () => {
|
||||
const [vulnerabilities, setVulnerabilities] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [pageNo, setPageNo] = useState(1)
|
||||
const [pageSize] = useState(10)
|
||||
const [totalCount, setTotalCount] = useState(0)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchVulnerabilities()
|
||||
}, [pageNo, searchTerm])
|
||||
|
||||
const fetchVulnerabilities = async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const response = await getVulnerabilities(pageNo, pageSize, searchTerm)
|
||||
const { data, total_count } = response.data.data
|
||||
setVulnerabilities(data)
|
||||
setTotalCount(total_count)
|
||||
} catch (err) {
|
||||
setError('获取漏洞列表失败')
|
||||
console.error('Fetch vulnerabilities error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (newPageNo) => {
|
||||
setPageNo(newPageNo)
|
||||
}
|
||||
|
||||
const handleSearch = (e) => {
|
||||
e.preventDefault()
|
||||
// 重置页码到第一页
|
||||
setPageNo(1)
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(totalCount / pageSize)
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-extrabold text-gray-900 mb-2">
|
||||
漏洞信息列表
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
当前共有 {totalCount} 个漏洞信息
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="mb-6">
|
||||
<form onSubmit={handleSearch} className="max-w-md mx-auto">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="搜索漏洞标题或描述..."
|
||||
className="block w-full pl-4 pr-12 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
>
|
||||
<svg className="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
|
||||
<div className="text-red-800 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{vulnerabilities.map((vuln) => (
|
||||
<div key={vuln.id} className="bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-300 ease-in-out">
|
||||
<Link to={`/vulns/${vuln.id}`} className="block h-full">
|
||||
<div className="p-6 h-full flex flex-col">
|
||||
<div className="flex justify-between items-start">
|
||||
<h3 className="text-lg font-bold text-gray-900 truncate">{vuln.title}</h3>
|
||||
<span className={`ml-2 px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
vuln.severity === 'Critical' ? 'bg-red-100 text-red-800' :
|
||||
vuln.severity === 'High' ? 'bg-orange-100 text-orange-800' :
|
||||
vuln.severity === 'Medium' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{vuln.severity}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-gray-600 text-sm flex-grow">
|
||||
{vuln.description.substring(0, 120)}...
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{vuln.tags && vuln.tags.slice(0, 3).map((tag, index) => (
|
||||
<span key={index} className="px-2 py-1 bg-indigo-100 text-indigo-800 text-xs font-medium rounded-full">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{vuln.tags && vuln.tags.length > 3 && (
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-800 text-xs font-medium rounded-full">
|
||||
+{vuln.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between text-sm">
|
||||
<div className="flex items-center">
|
||||
<svg className="h-4 w-4 text-gray-500 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<span className="text-gray-500">CVE: {vuln.cve || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg className="h-4 w-4 text-gray-500 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-gray-500">
|
||||
{vuln.pushed ? '已推送' : '未推送'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-xs text-gray-400">
|
||||
更新时间: {new Date(vuln.updated_at).toLocaleDateString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 分页组件 */}
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-700">
|
||||
显示第 {(pageNo - 1) * pageSize + 1} 到 {Math.min(pageNo * pageSize, totalCount)} 条记录,
|
||||
总共 {totalCount} 条记录
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => handlePageChange(pageNo - 1)}
|
||||
disabled={pageNo === 1}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||
pageNo === 1
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-300'
|
||||
}`}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span className="px-4 py-2 text-sm text-gray-700">
|
||||
第 {pageNo} 页,共 {totalPages} 页
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handlePageChange(pageNo + 1)}
|
||||
disabled={pageNo === totalPages}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-md ${
|
||||
pageNo === totalPages
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-300'
|
||||
}`}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VulnerabilityListPage
|
||||
11
assets/tailwind.config.js
Normal file
11
assets/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
21
assets/vite.config.js
Normal file
21
assets/vite.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:9000',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: '../public',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
})
|
||||
@@ -60,11 +60,15 @@ impl fmt::Display for Severity {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ListVulnInformationRequest {
|
||||
pub page_filter: PageFilter,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
impl ListVulnInformationRequest {
|
||||
pub fn new(page_filter: PageFilter) -> Self {
|
||||
ListVulnInformationRequest { page_filter }
|
||||
pub fn new(page_filter: PageFilter, search: Option<String>) -> Self {
|
||||
ListVulnInformationRequest {
|
||||
page_filter,
|
||||
search,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::{
|
||||
pub struct ListVulnInformationRequestBody {
|
||||
pub page_no: i32,
|
||||
pub page_size: i32,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
@@ -40,7 +41,7 @@ impl ListVulnInformationRequestBody {
|
||||
let page_no = PageNo::try_new(self.page_no)?;
|
||||
let page_size = PageSize::try_new(self.page_size)?;
|
||||
let page_filter = PageFilter::new(page_no, page_size);
|
||||
Ok(ListVulnInformationRequest::new(page_filter))
|
||||
Ok(ListVulnInformationRequest::new(page_filter, self.search))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,9 +71,15 @@ impl VulnRepository for Pg {
|
||||
self.pool.begin().await.change_context_lazy(|| {
|
||||
Error::Message("failed to begin transaction".to_string())
|
||||
})?;
|
||||
let vuln_informations =
|
||||
VulnInformationDao::filter_vulnfusion_information(&mut tx, &req.page_filter).await?;
|
||||
let count = VulnInformationDao::filter_vulnfusion_information_count(&mut tx).await?;
|
||||
let vuln_informations = VulnInformationDao::filter_vulnfusion_information(
|
||||
&mut tx,
|
||||
&req.page_filter,
|
||||
req.search.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let count =
|
||||
VulnInformationDao::filter_vulnfusion_information_count(&mut tx, req.search.as_deref())
|
||||
.await?;
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
|
||||
@@ -106,12 +106,22 @@ impl VulnInformationDao {
|
||||
pub async fn filter_vulnfusion_information(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
page_filter: &PageFilter,
|
||||
search: Option<&str>,
|
||||
) -> Result<Vec<VulnInformation>, Error> {
|
||||
let query_builder = DaoQueryBuilder::<Self>::new();
|
||||
let mut query_builder = DaoQueryBuilder::<Self>::new();
|
||||
let page_no = *page_filter.page_no().as_ref();
|
||||
let page_size = *page_filter.page_size().as_ref();
|
||||
let offset = (page_no - 1) * page_size;
|
||||
|
||||
// 添加搜索条件
|
||||
if let Some(search_term) = search {
|
||||
if !search_term.is_empty() {
|
||||
query_builder = query_builder
|
||||
.and_where_like("title", search_term)
|
||||
.and_where_like("description", search_term);
|
||||
}
|
||||
}
|
||||
|
||||
query_builder
|
||||
.order_by_desc("id")
|
||||
.limit_offset(page_size as i64, offset as i64)
|
||||
@@ -121,8 +131,19 @@ impl VulnInformationDao {
|
||||
|
||||
pub async fn filter_vulnfusion_information_count(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
search: Option<&str>,
|
||||
) -> Result<i64, Error> {
|
||||
let query_builder = DaoQueryBuilder::<Self>::new();
|
||||
let mut query_builder = DaoQueryBuilder::<Self>::new();
|
||||
|
||||
// 添加搜索条件
|
||||
if let Some(search_term) = search {
|
||||
if !search_term.is_empty() {
|
||||
query_builder = query_builder
|
||||
.and_where_like("title", search_term)
|
||||
.and_where_like("description", search_term);
|
||||
}
|
||||
}
|
||||
|
||||
query_builder.count(tx).await
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user