feat: newapi-dashboard 全栈项目初始化

为 new-api (LLM API 网关) 构建独立前端管理面板:
- React 18 + TypeScript + Vite + Ant Design 5 前端
- Node.js + Express + better-sqlite3 BFF 后端
- 登录页: 站点选择器 + UserID + AccessToken 认证
- 仪表盘: 用户信息、额度环形图、7天趋势图、模型饼图、令牌概览、日志时间线
- 账单页: 筛选日志表格、模型消耗柱状图、充值记录、CSV/PDF 导出(HMAC签名)
- 管理员站点管理: 站点 CRUD
- API 代理: 多站点切换,会话管理

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
LAMCLOD
2026-03-08 18:00:28 +08:00
commit f6036cab66
36 changed files with 10579 additions and 0 deletions

78
server/routes/auth.ts Normal file
View File

@@ -0,0 +1,78 @@
import { Router, Request, Response } from 'express'
import { v4 as uuidv4 } from 'uuid'
import db from '../db.js'
import { sessionAuth } from '../middleware/auth.js'
const router = Router()
// POST /api/auth/login
router.post('/login', async (req: Request, res: Response) => {
const { userId, accessToken, siteId } = req.body
if (!userId || !accessToken || !siteId) {
res.json({ success: false, message: '请填写完整信息' })
return
}
const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(siteId) as any
if (!site) {
res.json({ success: false, message: '站点不存在' })
return
}
try {
const response = await fetch(`${site.url}/api/user/self`, {
headers: { 'Authorization': accessToken }
})
const result = await response.json() as any
if (!result.success) {
res.json({ success: false, message: result.message || '认证失败请检查用户ID和令牌' })
return
}
const userInfo = result.data
if (userInfo.id !== Number(userId)) {
res.json({ success: false, message: '用户ID与令牌不匹配' })
return
}
const sessionId = uuidv4()
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
db.prepare(
'INSERT INTO sessions (id, user_id, access_token, site_id, site_url, user_info, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
).run(sessionId, userId, accessToken, siteId, site.url, JSON.stringify(userInfo), expiresAt)
res.json({
success: true,
data: {
sessionToken: sessionId,
userInfo,
site: { id: site.id, name: site.name, url: site.url }
}
})
} catch (error: any) {
res.json({ success: false, message: `无法连接站点: ${error.message}` })
}
})
// POST /api/auth/logout
router.post('/logout', sessionAuth, (req: Request, res: Response) => {
db.prepare('DELETE FROM sessions WHERE id = ?').run(req.session!.id)
res.json({ success: true })
})
// GET /api/auth/me
router.get('/me', sessionAuth, (req: Request, res: Response) => {
const site = db.prepare('SELECT id, name, url FROM sites WHERE id = ?').get(req.session!.site_id)
res.json({
success: true,
data: {
userInfo: JSON.parse(req.session!.user_info),
site
}
})
})
export default router