Files
rainblogweb/docs/refactor-plan.md
T

175 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RainWeb 优化重构实施计划(P0 / P1 / P2)
> 依据:数据库优化师(better-sqlite3 选型调研)、后端架构师(架构评审)、应用安全工程师(安全审计)、前端路由专项深挖 四份报告合并。
> 数据库路线已定:**better-sqlite3 + Node 22 LTS**(放弃 sql.js 与 PostgreSQL)。
> 前端路由:用户确认问题很大 → 按「方案 A 止血 → 方案 B 协议收编」两步走(见 P1-14 / P2-19)。
> 本计划仅执行顺序与细节,**未开始任何代码改动**。
## 全局前提(每阶段执行前)
1. 备份:`cp -r data data.bak-<日期>``data/rainweb.db` 是唯一数据文件)
2. 每阶段一个 git commitcommit message 中文,如 `P0: 删除 Web 更新链路,修复鉴权漏洞`
3. 无测试环境,验证方式 = curl 接口 + 浏览器刷新 + `node cli.js` 命令
---
## P0 紧急止损(独立项,可并行,约半天)
### P0-1 删除 Web 更新链路(消除 R6 + .git 覆盖风险)
- `server.js`:删除 84-165 行 `/api/update/check` + `/api/update/run`,保留 `/api/version`81-82 行)
- `public/js/admin.js`:删除 checkUpdate/runUpdate 函数及更新按钮 UI(约 692-731 行区域)
- `cli.js``cmdUpgrade` 保留 git pull 分支(走本地 Gitea origin),删除非 git 仓库的 zip 下载分支(266-346 行区域)
- 理由:用户即上游,zip 覆盖式更新是错误范式;`/api/update/run` 无鉴权=RCE 面,且 exclude 未排除 `.git` 会冲掉 git 历史
- 风险:低。注意前端引用一并删除,避免 404
- 验证:`curl /api/update/check` → 404;admin 面板打开控制台无报错;`node cli.js upgrade` 走 git pull 分支正常
### P0-2 JWT_SECRET 随机化(消除 R4
- `middleware/auth.js`:改为 `process.env.JWT_SECRET` 必须存在(长度 ≥32),否则启动失败;删除硬编码回退
- `server.js` `start()`:首次启动若无 `JWT_SECRET`,用 `crypto.randomBytes(48).toString('hex')` 生成并写入 `.env.json`
- 风险:低;已签发旧 token 全部失效(自部署可接受)
- 验证:删 `.env.json` 重启 → 自动生成 secret;登录 → `curl /api/auth/me` 200
### P0-3 setup 接管漏洞(消除 R5
- `routes/setup.js``POST /complete``authMiddleware, adminOnly``GET /setup/status` 响应移除 `default_password` 字段(只保留 `setup_complete`
- 风险:低
- 验证:未登录 `curl -X POST /api/setup/complete` → 401;登录后正常
### P0-4 proxy SSRF 最小防护(消除 R3 的 SSRF 部分)
- `routes/proxy.js``GET /fetch``authMiddleware, adminOnly``new URL(target)` 后校验 hostname 非 `127.0.0.1`/`::1`/`localhost`/`10.`/`192.168.`/`172.16-31.`/`169.254.`/`fc00:`/`fe80:`
- 风险:低(本功能只服务于 admin_links 面板嵌入)
- 验证:未登录请求 → 401`/api/proxy/fetch?url=http://127.0.0.1:3001/` → 拒绝
### P0-5 邮件验证码回显与滥用(消除 R7)
- `routes/email.js``send-verify` 响应删除 `code` 回显;验证码生成改 `crypto.randomInt(10000000, 100000000)`;补回"删除 pending 后重建"的 bug(或废弃该接口由 register 内联重发)
- 风险:低
- 验证:调 send-verify → 响应无 code 字段;邮件仍能收到验证码
---
## P1 安全加固 + 数据层统一(依赖顺序:11 → 12,其余独立)
### P1-6 上传白名单 + /uploads 防护头(消除 R1
- `routes/upload.js``shaFileUpload` 内校验 `path.extname(...).toLowerCase()``ALLOWED_EXT = ['.png','.jpg','.jpeg','.gif','.webp','.pdf','.zip','.txt','.md']` 内,否则报"不支持的文件类型";图片额外校验 `file.mimetype``image/` 开头
- `server.js:63``/uploads` 静态服务加 `X-Content-Type-Options: nosniff` + `Content-Security-Policy: sandbox; default-src 'none'`;非白名单扩展名强制 `Content-Disposition: attachment`
- 风险:低;已有历史上传文件不受影响(仅新上传受限)
- 验证:登录后上传 `x.html` → 400;上传 png → 200;访问 `/uploads/<hash>.png` 响应头含 nosniff
### P1-7 内容净化双端统一(消除 R2)
- `ssr.js`:引入 `dompurify + jsdom`(服务端),`renderSSR``marked.parse` 结果过 `DOMPurify.sanitize``ssrPage``title/siteName/ogDesc` 全部 HTML 转义,JSON-LD 用 `JSON.stringify` 序列化
- `public/js/render.js`:引入 DOMPurify(本地化到 public/js/vendor/ 或 CDN + SRI),`marked.parse` 结果同样过 sanitize
- 风险:中(渲染路径改动,需走查博客/论坛/SEO 页)
- 验证:发帖内容含 `<img src=x onerror=alert(1)>` → SSR 页与 SPA 页均不弹窗、标签被剥;正常 markdown 渲染不变
### P1-8 登录限流 + 服务端验证码(消除 R8)
- 新增 `express-rate-limit` 依赖:`/api/auth/login` 15 分钟 10 次;`/api/email/send-verify` 60s/次
- `routes/auth.js`login/register 按 `captcha_login`/`captcha_register` 设置服务端校验 captcha token(与 captcha.js 的 verify 存储联动,token 与 username 绑定);`routes/forum.js` 发帖同理(`captcha_forum`
- 删除 `verifyCaptchaToken` 桩函数(auth.js:39-52
- 风险:中(验证码链路前后端要同步改)
- 验证:连错密码 11 次 → 429;开启 captcha_login 后登录需先过验证码
### P1-9 密码箱解锁限流 + KDF 加固(消除 M1)
- `routes/passwords.js``/unlock` 加失败计数(3 次/5 分钟,指数退避);解锁会话滑动过期替换一次性 setTimeout
- PBKDF2 迭代 100k → 600kOWASP 2023 建议);注意同步执行阻塞事件循环,可接受(单用户站)
- 风险:低
- 验证:连续输错 PIN 3 次 → 锁定提示;正确解锁正常;旧 PIN 密文在 KDF 迭代变更后需重设(set-pin 已存在)
### P1-10 鉴权与信息收敛(消除 M2-M7)
- `middleware/auth.js``adminOnly` 增加 `db.get('SELECT role FROM users WHERE id=?')` 复查(M2
- `routes/blog.js``?all=1``authMiddleware, adminOnly``GET /posts/:id` 未发布仅 admin/作者可见(M3)
- 错误信息收敛:`routes/forum.js:21,35``server.js:163``routes/email.js:54,93` 对外统一 `{ error: '操作失败' }`,详情仅 console.errorM4
- `routes/email.js:17``auth.js:93``profile.js:25``proxy.js:22``rejectUnauthorized:false` 收敛为配置项(默认关闭校验仅当 SMTP 自签时开启)(M5)
- `public/js/passwords.js:113``forum.js:76`:内联 onclick 字符串插值改 `data-id` + `addEventListener` 委托(M6
- **S3-3 escapeHtml 引号问题**`escapeHtml` 不转义 `'`/`"`,用于属性/JS 字符串上下文可注入断链(forum.js:76、passwords.js:113、admin.js:74/402-404)——新增 `escapeAttr`(全转义)用于属性上下文,或随 M6 的 data-id 改造一并消除;帖子标题/分类为他人可见,属存储型 XSS 面
- `routes/profile.js`avatar 仅接受 `/uploads/avatars/` 站内路径(M7
- 风险:中;验证:逐项 curl 测试(未登录 401/403、草稿对匿名 404、报错无堆栈详情)
### P1-11 cli.js 统一数据层(ora-2 P1-1
- `cli.js`:删除 9-51 行重复封装(DB_PATH/getDb/dbRun/dbGet/dbAll),改为 `const db = require('./db')``cmdStatus/cmdPassword/cmdCaptcha/cmdConfig` 改用 `db.get/db.all`;各命令 `await getDb()`
- `data.db` 旧路径彻底退役(server.js 启动迁移逻辑保留,把旧文件搬进 data/)
- 风险:中低;验证:备份后依次跑 `node cli.js status/config/password/captcha` 全部成功且落库
### P1-12 schema_version 迁移框架(ora-2 P1-2,依赖 P1-11
- `db.js``initTables()` 收敛——"确保列存在"函数(幂等补列,逻辑同现有 try/catch)+ `PRAGMA user_version` 记录迁移版本;未来迁移写成 `{ version, up() }` 数组顺序执行
- 风险:中;验证:旧库启动自动补列无报错;连续启动两次幂等
### P1-13 deploy 脚本处置(ora-2 P1-3
- 删除 `deploy.sh` + `deploy.bat`(指向 GitHub 旧仓库,对自托管用户无意义);README 相关段落同步清理
- 风险:低;验证:`ls` 确认删除;README 无残留引用
### P1-14 前端路由止血(方案 A,约 0.5 天)
> 专项深挖结论:PJAX 只替换 `<main>`、目标页 `<script>` 从不执行,而 5 个 PJAX 壳页各自加载不同共享脚本子集 → 从某些页面进入时目标页依赖缺失(结构性缺陷 S1-3)。已确认 bug:S1-1 首页空白、S1-2 个人中心空白、S1-3 脚本依赖矩阵、S2-1 监听器重复累积、S2-2 论坛详情后退进 SSR 页、S2-4 滚动不恢复、S2-5 登录态/设置变化后导航不刷新、S2-6 login/register 白跳。
- **A1 PJAX_PATHS 收敛**`router.js` 只保留 5 个有 `_pageConfig` 的页面(`/``/blog.html``/forum.html``/admin.html``/passwords.html`);移除 `login.html`/`register.html`/`profile.html`(整页导航约定,根治 S1-2、S2-6)
- **A2 统一共享脚本集**5 个 PJAX 壳页 html 全部加载完整共享集(marked + render + captcha + music-embed + theme + api + nav + router),消除 S1-3 全部矩阵缺口
- **A3 首页逻辑外部化**`public/index.html:54-96` 内联 HOMEPAGE 抽为 `public/js/homepage.js` 并注册进 `_pageConfig`(修 S1-1;顺带合并 S3-4 与 blog.js 的 loadSidebar 重复)
- **A4 `_loadScript` 判重**`router.js:91-99` 加已加载检测,修 S2-1 监听器累积
- **A5 popstate 分支 + 滚动**popstate 识别 `e.state.forumPostId` 直接还原论坛详情(修 S2-2 不再 reload 进 SSR 页);navigate 时 `scrollTo(0,0)`(修 S2-4
- **A6 导航重渲染钩子**loadPage 完成后调 `NAV.render?.()`(修 S2-5
- 风险:低(login/register/profile 当前本就走整页兜底,无回归面);A2 可能触发 S3-2 重复定义,但各文件实现相同无碍
- 验证:passwords.html 整页进入 → PJAX 依次进博客(markdown 正常)/论坛(发帖弹验证码、详情正常)/首页;论坛详情后退回 SPA 列表无 reload;循环 PJAX 5 次 forum.js 只加载一次
---
## P2 数据库换轨 + 工程卫生(依赖 P1-11/P1-12
### P2-15 better-sqlite3 迁移(lib-1 调研结论)
- **前置**Node 升 22 LTS(宝塔 Node 版本管理器 / nvm);README 与 Docker 示例同步(`node:20``node:22`
- 依赖:`npm install better-sqlite3`(v13,预编译随包,宝塔 glibc x64 免编译)
- `db.js``initSqlJs()` 异步初始化 → `new Database(DB_PATH)` 同步;`run/get/all` 重写:
- `run``db.prepare(sql).run(...params)` + `info.lastInsertRowid`
- `get``stmt.get(...params)``all``stmt.all(...params)`
- 删除 `saveDb()`/`db.export()`(SQLite 事务原生落盘);返回签名不变,40+ 业务调用点零改动
- `initTables()` 的 CREATE TABLE IF NOT EXISTS 与幂等补列原样保留(兼容旧库)
- `cli.js`:同套改写(P1-11 统一后只需改 db.js 一处)
- `routes/import.js`:上传文件先落临时路径再 `new Database(tmpPath)` 只读打开,`exec` 数组结果改对象结果,用后 close
- **数据迁移**:零迁移——现有 `data/rainweb.db` 是标准 SQLite 文件,直接打开
- 风险:中;验证:备份 data/ 后启动 → 管理后台 CRUD 走一遍 → `node cli.js status/password``sqlite3 data/rainweb.db "PRAGMA integrity_check;"`
### P2-16 死代码 + 前端重复清理
- 删除 `routes/links.js`(引用不存在的 links 表,挂载即崩)、`public/js/main.js`(无引用)、`public/js/passwords.js` 内重复的 `deleteFromDetail`
- S3-1`register.html:85``:92` 重复调用 `CAPTCHA.checkRequired('register')`,删一行
- S3-2`escapeHtml/closeDialog/openDialog/showSnackbar` 在 nav/render/blog/forum/admin/passwords/index/profile 各定义一份 → 收敛到 nav.js(或公共 util)单份定义;admin.js 同文件内 6 个函数重复定义(setNavStyle/setCardStyle/previewWallpaperUrl/removeWallpaper/loadWallpaperList/selectUploadedWallpaper/uploadWallpaperadmin.js:179/263 等)删后一份
- 风险:低;验证:全站走查控制台无 404、无重复定义报错
### P2-17 版本单源 + 文档同步
- `VERSION` 文件为唯一源(server.js:81 已读它);`cli.js` HELP 版本改读 VERSION`package.json` version 与 VERSION 同步
- `README.md`Node 要求改 `>= 22 LTS`;删除更新/部署章节中 GitHub 引用
- `AGENTS.md`:数据层章节重写(better-sqlite3、无 saveDb、单文件直写、cli.js 已统一)
- 风险:低;验证:`node cli.js help``/api/version` 显示一致
### P2-18 可选加固(H 系列,按需确认)
- H1`helmet()` + 收紧 CSPCDN 引入 marked 的 5 个页面加 SRI)——建议做
- H3:验证码 `Math.random()``crypto.randomInt`auth.js:80、email.js:63、profile.js:68)——建议做,改动极小
- H9bcrypt cost 10 → 12;注册用户名格式校验——建议做
- H2JWT 从 localStorage 改 HttpOnly cookie——改动大(前端 api.js 全量),**默认不做**,列入后续
- H4:首启随机管理员密码——与 setup 向导冲突,**默认不做**(P0-3 已封堵接管面)
### P2-19 前端路由协议收编(方案 B,约 1.5-2 天,依赖 P1-14
> 方案 A 修完当前全部可复现 bug;方案 B 是防复发投资——解决"下一次加页面还会不会踩坑"。
- **B1 单一注册表**`PAGES` 配置对象 `path → { pjax, title, scripts, global, init }``PJAX_PATHS`/`_pageConfig`/脚本依赖全部由它派生;命中且 `pjax:true` 才拦截,其余整页导航(无兜底分支)
- **B2 页面结构规范**:每个 PJAX 页 `<main data-page="blog">`router 读 `data-page` 查注册表,不再硬编码路径数组;共享依赖在注册表声明,`_loadScript` 按"已加载集合"增量补齐
- **B3 页面逻辑全外部化**HOMEPAGEP1-14 A3 已抽)、profile 逻辑(profile.html:43-180)抽为独立 js;内联 `<script>` 仅保留极少量数据初始化;统一 `window[global].init()` 协议
- **B4 pushState/popstate 统一由 router 管理**:页面不再自行 pushState 非标准 state;论坛/博客详情作为"子路由"由注册表定义,popstate 由 router 解析还原(根治 S2-2/S2-3
- **B5 生命周期钩子**:注册表条目可选 `destroy()`,音乐嵌入/定时器/事件解绑在此处理(根治 S2-1 结构性累积)
- 整页应用(login/register/write/embed/forum-manage/setup)标记 `pjax:false` 不参与 PJAX,保留内联脚本
- 风险:中(popstate 语义变化与音乐嵌入销毁/重建是重点回归面;11 个 html 全动,必须全导航路径走查)
- 验证:全导航路径走查(每页进出、后退/前进、刷新直链、登录/退出、音乐嵌入显示与自动隐藏);控制台零报错
- 明确不做(方案 C):引入构建链/框架——违反无构建约定,对单人无测试项目负收益
---
```
P0-1..P0-5 (独立,并行) ≈ 半天
P1-6..P1-10 (安全加固,独立并行)
P1-11 cli统一 → P1-12 schema_version ≈ 1-2 天
P1-13 / P1-14(路由止血,独立) ≈ 0.5 天
P2-15 better-sqlite3(依赖 P1-11/12)≈ 1 天
P2-16 / P2-17 / P2-18(独立)
P2-19 路由协议收编(依赖 P1-14 ≈ 1.5-2 天
```
> 若时间有限:P0 + P1 全部完成后项目即处于"安全 + 可正常使用"状态(路由 8 个 bug 全修);P2-19 是防复发投资,可最后做或延后。
## 交付节奏
每个 P 阶段结束:跑通验证清单 → git commit → 可随时暂停(数据文件备份在手,sql.js→better-sqlite3 前任何时刻可回滚)。