const express = require('express'); const fs = require('fs'); const path = require('path'); const { authMiddleware, adminOnly } = require('../middleware/auth'); /* ============================================================ * 工作台「记事本」API(仅管理员) * * 存储:实体 .txt 文件,目录 frontend/src/tools/workbench/workspace/ * (.gitignore 已排除,不入库) * * GET /api/notes → 文件列表(按修改时间倒序) * GET /api/notes/:filename → 读取文件内容 { name, content, size, mtime } * POST /api/notes → 创建 { filename, content }(同名 409) * PUT /api/notes/:filename → 更新 { content }(不存在 404) * DELETE /api/notes/:filename → 删除 * * 安全:文件名只允许 .txt、拒绝路径穿越(/、\、..、隐藏文件) * ============================================================ */ const WORKSPACE_DIR = path.join(__dirname, '..', 'frontend', 'src', 'tools', 'workbench', 'workspace'); const router = express.Router(); function ensureDir() { fs.mkdirSync(WORKSPACE_DIR, { recursive: true }); } /** 文件名安全校验:返回规范化文件名(自动补 .txt),非法返回 null */ function sanitizeFilename(raw) { if (typeof raw !== 'string') return null; let name = raw.trim(); if (!name || name.length > 120) return null; if (name.includes('/') || name.includes('\\') || name.includes('\0')) return null; if (name === '.' || name === '..' || name.startsWith('.')) return null; if (!name.toLowerCase().endsWith('.txt')) name += '.txt'; // 兜底:解析后必须仍位于 workspace 目录内,杜绝路径穿越 const full = path.join(WORKSPACE_DIR, name); if (!full.startsWith(WORKSPACE_DIR + path.sep)) return null; return name; } /** 列出所有 .txt 笔记,按修改时间倒序 */ function listNotes() { ensureDir(); return fs.readdirSync(WORKSPACE_DIR, { withFileTypes: true }) .filter((d) => d.isFile() && d.name.toLowerCase().endsWith('.txt')) .map((d) => { const st = fs.statSync(path.join(WORKSPACE_DIR, d.name)); return { name: d.name, size: st.size, mtime: st.mtimeMs }; }) .sort((a, b) => b.mtime - a.mtime); } // 列表 router.get('/', authMiddleware, adminOnly, (req, res) => { try { res.json(listNotes()); } catch (e) { console.error('Notes list error:', e.message); res.status(500).json({ error: '读取笔记列表失败' }); } }); // 读取内容 router.get('/:filename', authMiddleware, adminOnly, (req, res) => { const name = sanitizeFilename(req.params.filename); if (!name) return res.status(400).json({ error: '文件名不合法' }); try { const full = path.join(WORKSPACE_DIR, name); if (!fs.existsSync(full)) return res.status(404).json({ error: '笔记不存在' }); const content = fs.readFileSync(full, 'utf8'); const st = fs.statSync(full); res.json({ name, content, size: st.size, mtime: st.mtimeMs }); } catch (e) { console.error('Note read error:', e.message); res.status(500).json({ error: '读取笔记失败' }); } }); // 创建 router.post('/', authMiddleware, adminOnly, (req, res) => { const name = sanitizeFilename(req.body && req.body.filename); if (!name) return res.status(400).json({ error: '文件名不合法' }); const content = typeof req.body.content === 'string' ? req.body.content : ''; try { ensureDir(); const full = path.join(WORKSPACE_DIR, name); if (fs.existsSync(full)) return res.status(409).json({ error: '同名笔记已存在' }); fs.writeFileSync(full, content, 'utf8'); const st = fs.statSync(full); res.json({ name, size: st.size, mtime: st.mtimeMs }); } catch (e) { console.error('Note create error:', e.message); res.status(500).json({ error: '创建笔记失败' }); } }); // 更新内容 router.put('/:filename', authMiddleware, adminOnly, (req, res) => { const name = sanitizeFilename(req.params.filename); if (!name) return res.status(400).json({ error: '文件名不合法' }); const content = typeof req.body.content === 'string' ? req.body.content : ''; try { const full = path.join(WORKSPACE_DIR, name); if (!fs.existsSync(full)) return res.status(404).json({ error: '笔记不存在' }); fs.writeFileSync(full, content, 'utf8'); const st = fs.statSync(full); res.json({ name, size: st.size, mtime: st.mtimeMs }); } catch (e) { console.error('Note update error:', e.message); res.status(500).json({ error: '保存笔记失败' }); } }); // 删除 router.delete('/:filename', authMiddleware, adminOnly, (req, res) => { const name = sanitizeFilename(req.params.filename); if (!name) return res.status(400).json({ error: '文件名不合法' }); try { const full = path.join(WORKSPACE_DIR, name); if (!fs.existsSync(full)) return res.status(404).json({ error: '笔记不存在' }); fs.unlinkSync(full); res.json({ message: '删除成功' }); } catch (e) { console.error('Note delete error:', e.message); res.status(500).json({ error: '删除笔记失败' }); } }); module.exports = router;