feat: 新增补丁管理和异常检测插件及相关功能

feat(protocol): 添加补丁管理和行为指标协议类型
feat(client): 实现补丁管理插件采集功能
feat(server): 添加补丁管理和异常检测API
feat(database): 新增补丁状态和异常检测相关表
feat(web): 添加补丁管理和异常检测前端页面
fix(security): 增强输入验证和防注入保护
refactor(auth): 重构认证检查逻辑
perf(service): 优化Windows服务恢复策略
style: 统一健康评分显示样式
docs: 更新知识库文档
This commit is contained in:
iven
2026-04-11 15:59:53 +08:00
parent b5333d8c93
commit 60ee38a3c2
49 changed files with 3988 additions and 461 deletions

View File

@@ -1,5 +1,7 @@
/**
* Shared API client with authentication and error handling
* Shared API client with cookie-based authentication.
* Tokens are managed via HttpOnly cookies set by the server —
* the frontend never reads or stores JWT tokens.
*/
const API_BASE = import.meta.env.VITE_API_BASE || ''
@@ -21,43 +23,37 @@ export class ApiError extends Error {
}
}
function getToken(): string | null {
const token = localStorage.getItem('token')
if (!token || token.trim() === '') return null
return token
}
function clearAuth() {
localStorage.removeItem('token')
localStorage.removeItem('refresh_token')
window.location.href = '/login'
}
let refreshPromise: Promise<boolean> | null = null
/** Cached user info from /api/auth/me */
let cachedUser: { id: number; username: string; role: string } | null = null
export function getCachedUser() {
return cachedUser
}
export function clearCachedUser() {
cachedUser = null
}
async function tryRefresh(): Promise<boolean> {
// Coalesce concurrent refresh attempts
if (refreshPromise) return refreshPromise
refreshPromise = (async () => {
const refreshToken = localStorage.getItem('refresh_token')
if (!refreshToken || refreshToken.trim() === '') return false
try {
const response = await fetch(`${API_BASE}/api/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
credentials: 'same-origin',
})
if (!response.ok) return false
const result = await response.json()
if (!result.success || !result.data?.access_token) return false
if (!result.success) return false
localStorage.setItem('token', result.data.access_token)
if (result.data.refresh_token) {
localStorage.setItem('refresh_token', result.data.refresh_token)
// Update cached user from refresh response
if (result.data?.user) {
cachedUser = result.data.user
}
return true
} catch {
@@ -74,13 +70,8 @@ async function request<T>(
path: string,
options: RequestInit = {},
): Promise<T> {
const token = getToken()
const headers = new Headers(options.headers || {})
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
if (options.body && typeof options.body === 'string') {
headers.set('Content-Type', 'application/json')
}
@@ -88,18 +79,21 @@ async function request<T>(
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers,
credentials: 'same-origin',
})
// Handle 401 - try refresh before giving up
if (response.status === 401) {
const refreshed = await tryRefresh()
if (refreshed) {
// Retry the original request with new token
const newToken = getToken()
headers.set('Authorization', `Bearer ${newToken}`)
const retryResponse = await fetch(`${API_BASE}${path}`, { ...options, headers })
const retryResponse = await fetch(`${API_BASE}${path}`, {
...options,
headers,
credentials: 'same-origin',
})
if (retryResponse.status === 401) {
clearAuth()
clearCachedUser()
window.location.href = '/login'
throw new ApiError(401, 'UNAUTHORIZED', 'Session expired')
}
const retryContentType = retryResponse.headers.get('content-type')
@@ -112,7 +106,8 @@ async function request<T>(
}
return retryResult.data as T
}
clearAuth()
clearCachedUser()
window.location.href = '/login'
throw new ApiError(401, 'UNAUTHORIZED', 'Session expired')
}
@@ -159,11 +154,12 @@ export const api = {
return request<T>(path, { method: 'DELETE' })
},
/** Login doesn't use the auth header */
async login(username: string, password: string): Promise<{ access_token: string; refresh_token: string; user: { id: number; username: string; role: string } }> {
/** Login — server sets HttpOnly cookies, we only get user info back */
async login(username: string, password: string): Promise<{ user: { id: number; username: string; role: string } }> {
const response = await fetch(`${API_BASE}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ username, password }),
})
@@ -172,12 +168,27 @@ export const api = {
throw new ApiError(response.status, 'LOGIN_FAILED', result.error || 'Login failed')
}
localStorage.setItem('token', result.data.access_token)
localStorage.setItem('refresh_token', result.data.refresh_token)
cachedUser = result.data.user
return result.data
},
logout() {
clearAuth()
/** Logout — server clears cookies */
async logout(): Promise<void> {
try {
await fetch(`${API_BASE}/api/auth/logout`, {
method: 'POST',
credentials: 'same-origin',
})
} catch {
// Ignore errors during logout
}
clearCachedUser()
},
/** Check current auth status via /api/auth/me */
async me(): Promise<{ user: { id: number; username: string; role: string }; expires_at: string }> {
const result = await request<{ user: { id: number; username: string; role: string }; expires_at: string }>('/api/auth/me')
cachedUser = (result as { user: { id: number; username: string; role: string } }).user
return result
},
}