feat: admin frontend - React + Vite, auth pages, user management, roles, permissions, webhooks, devices, logs

This commit is contained in:
2026-04-02 11:20:20 +08:00
parent dcc1f186f8
commit 4718980ab5
235 changed files with 35682 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
/**
* CSRF Token 管理
*
* CSRF 保护机制:
* 1. GET 请求获取 CSRF Token从 cookie 或 API
* 2. POST/PUT/DELETE 请求将 Token 添加到 X-CSRF-Token 头
*
* 注意:由于使用 Bearer Token 认证(存储在内存中),
* CSRF 风险相对较低,但为增强安全性仍建议对关键操作启用。
*/
// 注意:避免从 './client' 导入,防止循环依赖
// 使用原生 fetch 获取 CSRF Token
import { config } from '@/lib/config'
// CSRF Token 存储
let csrfToken: string | null = null
/**
* 获取 CSRF Token
*/
export function getCSRFToken(): string | null {
return csrfToken
}
/**
* 设置 CSRF Token
*/
export function setCSRFToken(token: string): void {
csrfToken = token
}
/**
* 从 cookie 中读取 CSRF Token
* Django/Laravel 等框架通常在 cookie 中设置 csrftoken
*/
export function getCSRFTokenFromCookie(): string | null {
if (typeof document === 'undefined') {
return null
}
const match = document.cookie.match(/csrftoken=([^;]+)/)
return match ? match[1] : null
}
/**
* 解析 API 基础 URL
* 注意:此函数复制自 client.ts 以避免循环依赖
*/
function resolveApiBaseUrl(): URL {
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
const rawBaseUrl = /^https?:\/\//i.test(config.apiBaseUrl)
? config.apiBaseUrl
: config.apiBaseUrl.startsWith('/')
? config.apiBaseUrl
: `/${config.apiBaseUrl}`
const baseUrl = new URL(rawBaseUrl, origin)
if (!baseUrl.pathname.endsWith('/')) {
baseUrl.pathname = `${baseUrl.pathname}/`
}
return baseUrl
}
/**
* 构建完整 URL
*/
function buildUrl(path: string): string {
const normalizedPath = path.replace(/^\/+/, '')
const url = new URL(normalizedPath, resolveApiBaseUrl())
return url.toString()
}
/**
* 初始化 CSRF Token
* 从 cookie 或 API 获取 Token 并存储
*/
export async function initCSRFToken(): Promise<string | null> {
// 优先从 cookie 获取
let token = getCSRFTokenFromCookie()
if (!token) {
try {
// 使用原生 fetch 避免循环依赖
const response = await fetch(buildUrl('/auth/csrf-token'), {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
})
if (response.ok) {
const result = await response.json()
// 后端返回字段名为 csrf_token
if (result.code === 0 && result.data?.csrf_token) {
token = result.data.csrf_token
}
}
} catch {
// API 不支持,使用 cookie 中的 token如果有
token = getCSRFTokenFromCookie()
}
}
if (token) {
setCSRFToken(token)
}
return token
}
/**
* 清除 CSRF Token登出时调用
*/
export function clearCSRFToken(): void {
csrfToken = null
}
/**
* CSRF Token 头名称
*/
export const CSRF_HEADER_NAME = 'X-CSRF-Token'
/**
* 获取带 CSRF Token 的请求头
* 用于 POST/PUT/DELETE 请求
*/
export function getCSRFHeaders(): Record<string, string> {
const token = csrfToken || getCSRFTokenFromCookie()
if (!token) {
return {}
}
return {
[CSRF_HEADER_NAME]: token
}
}
/**
* 需要 CSRF 保护的方法列表
*/
export const CSRF_PROTECTED_METHODS = ['POST', 'PUT', 'DELETE', 'PATCH']