Files
iboard/server/mcp/auth.js
T
jif2.zhang ec6c9c7677
Docker Build and Push / build-image (push) Successful in 4m34s
feat(AI接入): 内置 MCP 服务与个人 Token 管理
- 新增 /api/mcp (Streamable HTTP) 与 41 个工具: 项目/看板列/任务/清单/打卡; - 新增 McpToken 模型与迁移, DB 仅存 SHA-256 哈希, token 绑定用户; - 新增 /api/mcp-tokens 增删改查, 支持独立命名 (如 R1); - 「我的」页新增 AI 接入 (MCP) 区块: 地址复制/新建/一次性展示/删除; - 补充 docs/MCP.md 与样式
2026-11-09 10:41:08 +08:00

51 lines
1.8 KiB
JavaScript

// MCP 个人授权 token 的生成与校验
// - 明文只在创建时返回一次, 数据库仅存 SHA-256 哈希
// - token 与用户绑定, 使用该 token 调用 MCP 时拥有该用户的全部业务权限
import crypto from 'node:crypto'
import { prisma } from '../db.js'
const TOKEN_PREFIX = 'ibm_'
const BEARER_RE = /^Bearer\s+(.+)$/i
/** 生成新的 MCP token 明文 (256 bit 随机, base64url) */
export function generateMcpToken() {
return TOKEN_PREFIX + crypto.randomBytes(32).toString('base64url')
}
/** token 明文 -> 数据库存储的哈希 */
export function hashMcpToken(raw) {
return crypto.createHash('sha256').update(String(raw)).digest('hex')
}
/** 从 Authorization: Bearer xxx 中取出明文 token */
export function extractBearer(req) {
const h = req.headers?.authorization || ''
const m = String(h).match(BEARER_RE)
return m ? m[1].trim() : ''
}
/**
* 校验 MCP 请求的 token
* @returns {Promise<null | { tokenId: number, userId: number, user: { id: number, name: string, email: string } }>}
*/
export async function authenticateMcpToken(req) {
const raw = extractBearer(req)
if (!raw) return null
const row = await prisma.mcpToken.findUnique({
where: { tokenHash: hashMcpToken(raw) },
include: { user: { select: { id: true, name: true, email: true } } },
})
if (!row || !row.user) return null
// 最近使用时间: 每分钟最多写一次, 避免每次工具调用都产生写操作
const last = row.lastUsedAt ? new Date(row.lastUsedAt).getTime() : 0
if (Date.now() - last > 60_000) {
prisma.mcpToken
.update({ where: { id: row.id }, data: { lastUsedAt: new Date() } })
.catch((e) => console.warn('[mcp] 更新 lastUsedAt 失败:', e.message))
}
return { tokenId: row.id, userId: row.userId, user: row.user }
}