feat: 初始化项目基础架构和核心功能

- 添加项目基础结构:Cargo.toml、.gitignore、设备UID和密钥文件
- 实现前端Vue3项目结构:路由、登录页面、设备管理页面
- 添加核心协议定义(crates/protocol):设备状态、资产、USB事件等
- 实现客户端监控模块:系统状态收集、资产收集
- 实现服务端基础API和插件系统
- 添加数据库迁移脚本:设备管理、资产跟踪、告警系统等
- 实现前端设备状态展示和基本交互
- 添加使用时长统计和水印功能插件
This commit is contained in:
iven
2026-04-05 00:57:51 +08:00
commit fd6fb5cca0
87 changed files with 19576 additions and 0 deletions

126
web/src/lib/api.ts Normal file
View File

@@ -0,0 +1,126 @@
/**
* Shared API client with authentication and error handling
*/
const API_BASE = import.meta.env.VITE_API_BASE || ''
export interface ApiResult<T> {
success: boolean
data?: T
error?: string
}
export class ApiError extends Error {
constructor(
public status: number,
public code: string,
message: string,
) {
super(message)
this.name = 'ApiError'
}
}
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'
}
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')
}
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers,
})
// Handle 401 - token expired or invalid
if (response.status === 401) {
clearAuth()
throw new ApiError(401, 'UNAUTHORIZED', 'Session expired')
}
// Handle 403 - insufficient permissions
if (response.status === 403) {
throw new ApiError(403, 'FORBIDDEN', 'Insufficient permissions')
}
// Handle non-JSON responses (502, 503, etc.)
const contentType = response.headers.get('content-type')
if (!contentType || !contentType.includes('application/json')) {
throw new ApiError(response.status, 'NON_JSON_RESPONSE', `Server returned ${response.status}`)
}
const result: ApiResult<T> = await response.json()
if (!result.success) {
throw new ApiError(response.status, 'API_ERROR', result.error || 'Unknown error')
}
return result.data as T
}
export const api = {
get<T>(path: string): Promise<T> {
return request<T>(path)
},
post<T>(path: string, body?: unknown): Promise<T> {
return request<T>(path, {
method: 'POST',
body: body ? JSON.stringify(body) : undefined,
})
},
put<T>(path: string, body?: unknown): Promise<T> {
return request<T>(path, {
method: 'PUT',
body: body ? JSON.stringify(body) : undefined,
})
},
delete<T = void>(path: string): Promise<T> {
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 } }> {
const response = await fetch(`${API_BASE}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
})
const result = await response.json()
if (!result.success) {
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)
return result.data
},
logout() {
clearAuth()
},
}