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:
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user