// AI 接入 (MCP): 个人授权 token 管理 (登录用户本人) import { Router } from 'express' import { prisma } from '../db.js' import { authRequired } from '../middleware.js' import { generateMcpToken, hashMcpToken } from '../mcp/auth.js' const router = Router() router.use(authRequired) router.use((_req, res, next) => { res.set('Cache-Control', 'no-store') next() }) const NAME_MAX = 32 function publicToken(row) { return { id: row.id, name: row.name, prefix: row.prefix, lastUsedAt: row.lastUsedAt, createdAt: row.createdAt, } } function cleanName(v) { const name = String(v || '').trim() if (!name) return { error: 'Token 名称不能为空' } if (name.length > NAME_MAX) return { error: `Token 名称不能超过 ${NAME_MAX} 字` } return { name } } // GET /api/mcp-tokens 当前用户的全部 token (不含明文) router.get('/', async (req, res, next) => { try { const rows = await prisma.mcpToken.findMany({ where: { userId: req.user.sub }, orderBy: { createdAt: 'asc' }, }) res.json({ items: rows.map(publicToken) }) } catch (e) { next(e) } }) // POST /api/mcp-tokens { name } 新建 token, 明文仅在此响应中返回一次 router.post('/', async (req, res, next) => { try { const userId = req.user.sub const { name, error } = cleanName(req.body?.name) if (error) return res.status(400).json({ error }) const dup = await prisma.mcpToken.findFirst({ where: { userId, name } }) if (dup) return res.status(409).json({ error: `Token 名称「${name}」已存在` }) const raw = generateMcpToken() const row = await prisma.mcpToken.create({ data: { userId, name, prefix: raw.slice(0, 12), tokenHash: hashMcpToken(raw), }, }) res.json({ item: publicToken(row), token: raw }) } catch (e) { if (e?.code === 'P2002') return res.status(409).json({ error: 'Token 名称已存在' }) next(e) } }) // PATCH /api/mcp-tokens/:id { name } 重命名 router.patch('/:id', async (req, res, next) => { try { const userId = req.user.sub const id = Number(req.params.id) const { name, error } = cleanName(req.body?.name) if (error) return res.status(400).json({ error }) const exist = await prisma.mcpToken.findFirst({ where: { id, userId } }) if (!exist) return res.status(404).json({ error: 'Token 不存在' }) const dup = await prisma.mcpToken.findFirst({ where: { userId, name, NOT: { id } } }) if (dup) return res.status(409).json({ error: `Token 名称「${name}」已存在` }) const row = await prisma.mcpToken.update({ where: { id }, data: { name } }) res.json({ item: publicToken(row) }) } catch (e) { if (e?.code === 'P2002') return res.status(409).json({ error: 'Token 名称已存在' }) next(e) } }) // DELETE /api/mcp-tokens/:id 删除后立即失效 router.delete('/:id', async (req, res, next) => { try { const userId = req.user.sub const id = Number(req.params.id) const exist = await prisma.mcpToken.findFirst({ where: { id, userId } }) if (!exist) return res.status(404).json({ error: 'Token 不存在' }) await prisma.mcpToken.delete({ where: { id } }) res.json({ ok: true }) } catch (e) { next(e) } }) export default router