feat: 核心模块增强 — 加密 + 运行时配置 + 日志缓冲
- crypto.py: API Key 加密/解密工具 - runtime_config.py: 运行时动态配置管理 - log_buffer.py: 内存日志缓冲区 - config.py: 新增加密配置项 - http_client.py: 增强重试和错误处理
This commit is contained in:
+48
-33
@@ -1,33 +1,16 @@
|
||||
/**
|
||||
* Admin 后台管理系统 - 统一 API 客户端
|
||||
*
|
||||
* 写入型/高成本接口(采集、回测、结算)受 X-API-Key 保护:
|
||||
* 密钥在「系统配置」页设置,存于本机 localStorage,每次请求自动附带。
|
||||
* 鉴权:通过 POST /api/v1/auth/login 用密码换取 HttpOnly Cookie 会话,
|
||||
* 同源请求自动携带 Cookie,无需手动管理密钥。
|
||||
* 收到 401 时广播 `profeto:unauthorized` 事件,由 AdminLayout 切回登录页。
|
||||
*/
|
||||
|
||||
const API_BASE = '/api/v1'
|
||||
const TIMEOUT_MS = 30_000
|
||||
|
||||
const ADMIN_KEY_STORAGE = 'profeto_admin_key'
|
||||
|
||||
/** 读取本机保存的管理员密钥 */
|
||||
export function getAdminKey(): string {
|
||||
try {
|
||||
return localStorage.getItem(ADMIN_KEY_STORAGE) ?? ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存/清除管理员密钥(传空字符串即清除) */
|
||||
export function setAdminKey(key: string): void {
|
||||
try {
|
||||
if (key) localStorage.setItem(ADMIN_KEY_STORAGE, key)
|
||||
else localStorage.removeItem(ADMIN_KEY_STORAGE)
|
||||
} catch {
|
||||
/* 隐私模式等场景下不可用,静默忽略 */
|
||||
}
|
||||
}
|
||||
/** 会话失效事件名,AdminLayout 监听后弹出登录页 */
|
||||
export const UNAUTHORIZED_EVENT = 'profeto:unauthorized'
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
@@ -40,7 +23,10 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
async function request<T>(
|
||||
path: string,
|
||||
options: RequestInit & { timeoutMs?: number } = {},
|
||||
): Promise<T> {
|
||||
// 修复: 正确拼接 API_BASE
|
||||
const url = path.startsWith('http')
|
||||
? path
|
||||
@@ -48,18 +34,17 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
? path // 已经是绝对路径(如 /health)
|
||||
: `${API_BASE}${path}`
|
||||
|
||||
const { timeoutMs = TIMEOUT_MS, ...fetchOptions } = options
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
||||
|
||||
try {
|
||||
const adminKey = getAdminKey()
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
...fetchOptions,
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(adminKey ? { 'X-API-Key': adminKey } : {}),
|
||||
...options.headers,
|
||||
...fetchOptions.headers,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -75,7 +60,8 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
? String((detail as { detail: unknown }).detail)
|
||||
: `HTTP ${res.status}: ${res.statusText}`
|
||||
if (res.status === 401) {
|
||||
message += '\n请在「系统配置」页填写管理员密钥后重试。'
|
||||
message += '\n登录已过期,请重新登录。'
|
||||
window.dispatchEvent(new CustomEvent(UNAUTHORIZED_EVENT))
|
||||
}
|
||||
throw new ApiError(message, res.status, detail)
|
||||
}
|
||||
@@ -102,11 +88,40 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
|
||||
export const api = {
|
||||
get: <T>(path: string) => request<T>(path),
|
||||
post: <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }),
|
||||
put: <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, { method: 'PUT', body: body ? JSON.stringify(body) : undefined }),
|
||||
post: <T>(path: string, body?: unknown, opts?: { timeoutMs?: number }) =>
|
||||
request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined, ...opts }),
|
||||
put: <T>(path: string, body?: unknown, opts?: { timeoutMs?: number }) =>
|
||||
request<T>(path, { method: 'PUT', body: body ? JSON.stringify(body) : undefined, ...opts }),
|
||||
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
||||
}
|
||||
|
||||
// ── 认证 ────────────────────────────────────────────────────────
|
||||
|
||||
/** 密码登录,成功后服务端写入 HttpOnly 会话 Cookie */
|
||||
export function login(password: string): Promise<{ ok: boolean }> {
|
||||
return api.post(`${API_BASE}/auth/login`, { password })
|
||||
}
|
||||
|
||||
/** 退出登录,清除会话 Cookie */
|
||||
export function logout(): Promise<{ ok: boolean }> {
|
||||
return api.post(`${API_BASE}/auth/logout`)
|
||||
}
|
||||
|
||||
/** 探测当前登录状态 */
|
||||
export function fetchAuthState(): Promise<{
|
||||
authenticated: boolean
|
||||
enabled: boolean
|
||||
password_origin?: 'db' | 'env' | 'none'
|
||||
}> {
|
||||
return api.get(`${API_BASE}/auth/me`)
|
||||
}
|
||||
|
||||
/** 修改管理员密码(成功后所有会话失效,需重新登录) */
|
||||
export function changePassword(currentPassword: string, newPassword: string): Promise<{ ok: boolean; message: string }> {
|
||||
return api.post(`${API_BASE}/auth/change-password`, {
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
})
|
||||
}
|
||||
|
||||
export { API_BASE }
|
||||
|
||||
Reference in New Issue
Block a user