// 业务 REST API: 鉴权 + 衣物/搭配/日历/统计/MCP // 客户端不再持有数据库, 全部数据经此接口读写 (瘦客户端) import { Router } from 'express' import * as auth from '../auth.js' import { dbCreateClothing, dbListClothing, getClothing, dbUpdateClothing, dbDeleteClothing, dbCreateOutfit, dbListOutfits, dbUpsertDaily, dbListDaily, dbGetStats, mcpTools, mcpCall, } from '../db.js' const router = Router() const asyncH = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next) // ── 鉴权中间件: Authorization: Bearer ── async function requireAuth(req, res, next) { try { const h = req.headers.authorization || '' const token = h.startsWith('Bearer ') ? h.slice(7).trim() : null const user = await auth.resolveSession(token) if (!user) return res.status(401).json({ error: '未登录或登录已过期' }) req.user = user next() } catch (e) { next(e) } } // ── Auth ── router.post('/auth/send-code', asyncH(async (req, res) => { const target = String(req.body?.target || '').trim() if (!target) return res.status(400).json({ error: '请提供邮箱或手机号' }) res.json({ message: auth.sendVerification(target) }) })) router.post('/auth/register', asyncH(async (req, res) => { const { email, phone, code, nickname } = req.body || {} res.json(await auth.register({ email: email || null, phone: phone || null, code, nickname })) })) router.post('/auth/login', asyncH(async (req, res) => { const { email, phone, code } = req.body || {} res.json(await auth.login({ email: email || null, phone: phone || null, code })) })) router.post('/auth/logout', requireAuth, asyncH(async (req, res) => { const h = req.headers.authorization || '' const token = h.startsWith('Bearer ') ? h.slice(7).trim() : null await auth.logout(token) res.json({ success: true }) })) router.get('/auth/me', requireAuth, asyncH(async (req, res) => { res.json({ user: req.user }) })) // ── Clothing ── router.get('/clothing', requireAuth, asyncH(async (req, res) => { res.json(await dbListClothing(req.user.id, req.query.category || null)) })) router.post('/clothing', requireAuth, asyncH(async (req, res) => { const b = req.body || {} if (!b.name || !b.category || !b.color) return res.status(400).json({ error: '缺少必填字段' }) res.json(await dbCreateClothing(req.user.id, b)) })) router.get('/clothing/:id', requireAuth, asyncH(async (req, res) => { res.json(await getClothing(req.user.id, req.params.id)) })) router.put('/clothing/:id', requireAuth, asyncH(async (req, res) => { const b = req.body || {} await dbUpdateClothing(req.user.id, req.params.id, b.name, b.category, b.color) res.json({ success: true }) })) router.delete('/clothing/:id', requireAuth, asyncH(async (req, res) => { await dbDeleteClothing(req.user.id, req.params.id) res.json({ success: true }) })) // ── Outfits ── router.get('/outfits', requireAuth, asyncH(async (req, res) => { res.json(await dbListOutfits(req.user.id)) })) router.post('/outfits', requireAuth, asyncH(async (req, res) => { const b = req.body || {} if (!b.name || !Array.isArray(b.item_ids)) return res.status(400).json({ error: '缺少必填字段' }) res.json(await dbCreateOutfit(req.user.id, b)) })) // ── Daily Records ── router.get('/daily', requireAuth, asyncH(async (req, res) => { const limit = Math.min(Number(req.query.limit) || 30, 500) res.json(await dbListDaily(req.user.id, limit)) })) router.post('/daily', requireAuth, asyncH(async (req, res) => { const b = req.body || {} if (!b.date) return res.status(400).json({ error: '缺少 date' }) res.json(await dbUpsertDaily(req.user.id, b)) })) // ── Stats ── router.get('/stats', requireAuth, asyncH(async (req, res) => { res.json(await dbGetStats(req.user.id)) })) // ── MCP ── router.get('/mcp/tools', requireAuth, asyncH(async (req, res) => { res.json(mcpTools()) })) router.post('/mcp/call', requireAuth, asyncH(async (req, res) => { const { tool_name, args } = req.body || {} if (!tool_name) return res.status(400).json({ error: '缺少 tool_name' }) const r = await mcpCall(tool_name, args || {}, req.user.id) res.json(r.value) })) export default router