// Storage 业务 REST API: 家庭 / 盒子 / 物品 / 分类 / 转移 / 统计 / MCP // 权限模型: 管理员(admin)可写, 成员(member)只读。服务端为唯一权威。 import { Router } from 'express' import * as auth from '../auth.js' import { finalizeImages, deleteImageFiles } from './images.js' import { // users/sessions dbGetUserById, dbUpdateUserNickname, // families dbCreateFamily, dbListFamiliesForUser, dbGetFamily, dbUpdateFamily, dbDeleteFamily, dbGetRole, dbAddMember, dbRemoveMember, dbSetRole, dbSetInviteCode, dbJoinByCode, dbListMembers, // categories dbListCategories, dbCreateCategory, dbUpdateCategory, dbDeleteCategory, // boxes dbCreateBox, dbGetBox, dbUpdateBox, dbMoveBox, dbSetBoxPosition, dbDeleteBox, dbListChildBoxes, dbBuildTree, dbBoxPath, // items dbCreateItem, dbGetItem, dbListItems, dbUpdateItem, dbDeleteItem, dbMoveItem, // transfer dbTransfer, dbListTransferLogs, // stats / mcp dbGetFamilyStats, mcpTools, mcpCall, uu, } 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 req.token = token next() } catch (e) { next(e) } } // 家庭成员校验: familyId 取 URL 中的 :id async function requireMember(req, res, next) { const role = await dbGetRole(req.params.id, req.user.id) if (!role) return res.status(403).json({ error: '你不是该家庭成员' }) req.role = role next() } // 家庭管理员校验 (所有写操作) async function requireAdmin(req, res, next) { const role = await dbGetRole(req.params.id, req.user.id) if (role !== 'admin') return res.status(403).json({ error: '仅管理员可执行此操作' }) req.role = role next() } // ── 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) => { await auth.logout(req.token) res.json({ success: true }) })) router.get('/auth/me', requireAuth, asyncH(async (req, res) => { res.json({ user: await dbGetUserById(req.user.id) }) })) router.put('/auth/me', requireAuth, asyncH(async (req, res) => { const nickname = String(req.body?.nickname || '').trim() if (!nickname) return res.status(400).json({ error: '昵称不能为空' }) await dbUpdateUserNickname(req.user.id, nickname) res.json({ user: await dbGetUserById(req.user.id) }) })) // ── Families ── router.get('/families', requireAuth, asyncH(async (req, res) => { res.json(await dbListFamiliesForUser(req.user.id)) })) router.post('/families', requireAuth, asyncH(async (req, res) => { const name = String(req.body?.name || '').trim() if (!name) return res.status(400).json({ error: '请输入家庭名称' }) res.json(await dbCreateFamily(req.user.id, name, req.body?.icon || '🏠')) })) router.get('/families/:id', requireAuth, requireMember, asyncH(async (req, res) => { res.json({ family: await dbGetFamily(req.params.id), members: await dbListMembers(req.params.id), my_role: req.role, }) })) router.put('/families/:id', requireAuth, requireAdmin, asyncH(async (req, res) => { const name = String(req.body?.name || '').trim() if (!name) return res.status(400).json({ error: '请输入家庭名称' }) await dbUpdateFamily(req.params.id, { name, icon: req.body?.icon || '🏠' }) res.json({ success: true, family: await dbGetFamily(req.params.id) }) })) router.delete('/families/:id', requireAuth, requireAdmin, asyncH(async (req, res) => { const family = await dbGetFamily(req.params.id) if (family.owner_user_id !== req.user.id) return res.status(403).json({ error: '仅创建者可解散家庭' }) await dbDeleteFamily(req.params.id) res.json({ success: true }) })) // 通过邀请码加入 router.post('/families/:id/join', requireAuth, asyncH(async (req, res) => { const code = String(req.body?.invite_code || '').trim().toUpperCase() if (!code) return res.status(400).json({ error: '请输入邀请码' }) res.json(await dbJoinByCode(code, req.user.id)) })) // 邀请码管理 (管理员) router.post('/families/:id/invite', requireAuth, requireAdmin, asyncH(async (req, res) => { const action = req.body?.action || 'refresh' if (action === 'clear') { await dbSetInviteCode(req.params.id, null) return res.json({ invite_code: null }) } let code do { code = 'ST-' + uu().replaceAll('-', '').slice(0, 8).toUpperCase() } while (code.includes('0') || code.includes('O')) await dbSetInviteCode(req.params.id, code) res.json({ invite_code: code }) })) router.post('/families/:id/members/:userId/role', requireAuth, requireAdmin, asyncH(async (req, res) => { const role = req.body?.role if (!['admin', 'member'].includes(role)) return res.status(400).json({ error: 'role 非法' }) const family = await dbGetFamily(req.params.id) const targetExists = (await dbListMembers(req.params.id)).some((m) => m.user_id === req.params.userId) if (!targetExists) return res.status(404).json({ error: '成员不存在' }) if (req.params.userId === family.owner_user_id) return res.status(403).json({ error: '不能修改创建者角色' }) await dbSetRole(req.params.id, req.params.userId, role) res.json({ success: true }) })) router.post('/families/:id/members/:userId/add', requireAuth, requireAdmin, asyncH(async (req, res) => { await dbAddMember(req.params.id, req.params.userId, req.body?.role || 'member') res.json({ success: true }) })) // 移除成员; userId === 'me' 表示自己离开 router.delete('/families/:id/members/:userId', requireAuth, requireAdmin, asyncH(async (req, res) => { const family = await dbGetFamily(req.params.id) const target = req.params.userId === 'me' ? req.user.id : req.params.userId if (target === family.owner_user_id) return res.status(403).json({ error: '创建者不能离开/被移除, 请先解散家庭' }) if (target === req.user.id && req.params.userId === 'me') { await dbRemoveMember(req.params.id, target) return res.json({ success: true, left: true }) } await dbRemoveMember(req.params.id, target) res.json({ success: true }) })) // ── Categories ── router.get('/families/:id/categories', requireAuth, requireMember, asyncH(async (req, res) => { res.json(await dbListCategories(req.params.id)) })) router.post('/families/:id/categories', requireAuth, requireAdmin, asyncH(async (req, res) => { const name = String(req.body?.name || '').trim() if (!name) return res.status(400).json({ error: '请输入分类名称' }) res.json(await dbCreateCategory(req.params.id, name)) })) router.put('/families/:id/categories/:catId', requireAuth, requireAdmin, asyncH(async (req, res) => { const name = String(req.body?.name || '').trim() if (!name) return res.status(400).json({ error: '请输入分类名称' }) await dbUpdateCategory(req.params.id, req.params.catId, name) res.json({ success: true }) })) router.delete('/families/:id/categories/:catId', requireAuth, requireAdmin, asyncH(async (req, res) => { await dbDeleteCategory(req.params.id, req.params.catId) res.json({ success: true }) })) // ── Boxes ── // 直接子盒 (根层 = 不带 parent 参数) router.get('/families/:id/boxes', requireAuth, requireMember, asyncH(async (req, res) => { if (req.query.tree === '1') return res.json(await dbBuildTree(req.params.id)) const parentId = req.query.parent && req.query.parent !== 'root' ? String(req.query.parent) : null res.json(await dbListChildBoxes(req.params.id, parentId)) })) router.get('/families/:id/boxes/:boxId', requireAuth, requireMember, asyncH(async (req, res) => { const box = await dbGetBox(req.params.id, req.params.boxId) if (!box) return res.status(404).json({ error: '盒子不存在' }) const [path, children] = await Promise.all([ dbBoxPath(req.params.id, req.params.boxId), dbListChildBoxes(req.params.id, req.params.boxId), ]) res.json({ box, path, children }) })) router.post('/families/:id/boxes', requireAuth, requireAdmin, asyncH(async (req, res) => { const b = req.body || {} if (!b.name) return res.status(400).json({ error: '请输入盒子名称' }) res.json(await dbCreateBox(req.params.id, { name: String(b.name).trim(), parent_box_id: b.parent_box_id || null, icon: b.icon, color: b.color, pos_x: b.pos_x, pos_y: b.pos_y, width: b.width ?? null, height: b.height ?? null, })) })) router.put('/families/:id/boxes/:boxId', requireAuth, requireAdmin, asyncH(async (req, res) => { const b = req.body || {} const patch = {} if (b.name !== undefined) patch.name = String(b.name).trim() if (b.icon !== undefined) patch.icon = b.icon if (b.color !== undefined) patch.color = b.color if (b.width !== undefined) patch.width = b.width if (b.height !== undefined) patch.height = b.height if (!Object.keys(patch).length) return res.status(400).json({ error: '没有可更新的字段' }) await dbUpdateBox(req.params.id, req.params.boxId, patch) res.json({ success: true, box: await dbGetBox(req.params.id, req.params.boxId) }) })) // 整盒移动 (跨容器 / 根层) router.put('/families/:id/boxes/:boxId/move', requireAuth, requireAdmin, asyncH(async (req, res) => { const b = req.body || {} const parent = b.parent_box_id && b.parent_box_id !== 'root' ? String(b.parent_box_id) : null await dbMoveBox(req.params.id, req.params.boxId, parent, b.pos_x ?? 0, b.pos_y ?? 0) res.json({ success: true, box: await dbGetBox(req.params.id, req.params.boxId) }) })) // 仅调整位置 (画布拖拽) router.put('/families/:id/boxes/:boxId/position', requireAuth, requireAdmin, asyncH(async (req, res) => { const b = req.body || {} await dbSetBoxPosition(req.params.id, req.params.boxId, b.pos_x ?? 0, b.pos_y ?? 0) res.json({ success: true }) })) router.delete('/families/:id/boxes/:boxId', requireAuth, requireAdmin, asyncH(async (req, res) => { await dbDeleteBox(req.params.id, req.params.boxId) res.json({ success: true }) })) // ── Items ── router.get('/families/:id/items', requireAuth, requireMember, asyncH(async (req, res) => { const q = req.query const filters = { boxId: q.box && q.box !== 'root' ? String(q.box) : null, recursive: q.recursive === '1', looseOnly: q.loose === '1', categoryId: q.category || null, tag: q.tag || null, q: q.q ? String(q.q) : null, favorite: q.favorite === '1', expiring: q.expiring === '1', limit: q.limit, } res.json(await dbListItems(req.params.id, filters)) })) router.post('/families/:id/items', requireAuth, requireAdmin, asyncH(async (req, res) => { const b = req.body || {} if (!b.name) return res.status(400).json({ error: '请输入物品名称' }) b.images = finalizeImages(b.images || [], req.params.id) res.json(await dbCreateItem(req.params.id, b)) })) router.get('/families/:id/items/:itemId', requireAuth, requireMember, asyncH(async (req, res) => { const item = await dbGetItem(req.params.id, req.params.itemId) if (!item) return res.status(404).json({ error: '物品不存在' }) const path = item.box_id ? await dbBoxPath(req.params.id, item.box_id) : [] res.json({ item, box_path: path }) })) router.put('/families/:id/items/:itemId', requireAuth, requireAdmin, asyncH(async (req, res) => { const b = req.body || {} if (b.images !== undefined) b.images = finalizeImages(b.images || [], req.params.id) await dbUpdateItem(req.params.id, req.params.itemId, b) res.json({ success: true, item: await dbGetItem(req.params.id, req.params.itemId) }) })) router.delete('/families/:id/items/:itemId', requireAuth, requireAdmin, asyncH(async (req, res) => { const item = await dbGetItem(req.params.id, req.params.itemId) if (item?.images?.length) deleteImageFiles(item.images) await dbDeleteItem(req.params.id, req.params.itemId) res.json({ success: true }) })) // 物品移动 (换盒 / 根层) router.put('/families/:id/items/:itemId/move', requireAuth, requireAdmin, asyncH(async (req, res) => { const b = req.body || {} const boxId = b.box_id && b.box_id !== 'root' ? String(b.box_id) : null await dbMoveItem(req.params.id, req.params.itemId, boxId) res.json({ success: true, box_id: boxId }) })) // ── Transfer (跨家庭转移) ── // 权限: 操作者必须同时是 源家庭管理员 与 目标家庭管理员 (见 §14 决策点 2) router.post('/families/:id/transfer', requireAuth, requireAdmin, asyncH(async (req, res) => { const b = req.body || {} const dstFamilyId = String(b.dstFamilyId || '') if (!dstFamilyId) return res.status(400).json({ error: '缺少目标家庭' }) const dstRole = await dbGetRole(dstFamilyId, req.user.id) if (dstRole !== 'admin') return res.status(403).json({ error: '仅能转移到自己拥有管理权限的家庭' }) const dstParent = b.dstParentBoxId && b.dstParentBoxId !== 'root' ? String(b.dstParentBoxId) : null const r = await dbTransfer(req.params.id, { type: b.type, entityId: String(b.entityId || ''), dstFamilyId, dstParentBoxId: dstParent, posX: b.pos_x, posY: b.pos_y, operatorUserId: req.user.id, }) res.json(r) })) router.get('/families/:id/transfer-logs', requireAuth, requireAdmin, asyncH(async (req, res) => { res.json(await dbListTransferLogs(req.params.id)) })) // ── Stats ── router.get('/families/:id/stats', requireAuth, requireMember, asyncH(async (req, res) => { res.json(await dbGetFamilyStats(req.params.id)) })) // ── MCP (P1: AI 助手查询) ── 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