Files
rainblogweb/docs/refactor-plan.md
T

15 KiB
Raw Blame History

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/version81-82 行)
  • public/js/admin.js:删除 checkUpdate/runUpdate 函数及更新按钮 UI(约 692-731 行区域)
  • cli.jscmdUpgrade 保留 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.jsPOST /completeauthMiddleware, adminOnlyGET /setup/status 响应移除 default_password 字段(只保留 setup_complete
  • 风险:低
  • 验证:未登录 curl -X POST /api/setup/complete → 401;登录后正常

P0-4 proxy SSRF 最小防护(消除 R3 的 SSRF 部分)

  • routes/proxy.jsGET /fetchauthMiddleware, adminOnlynew 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.jssend-verify 响应删除 code 回显;验证码生成改 crypto.randomInt(10000000, 100000000);补回"删除 pending 后重建"的 bug(或废弃该接口由 register 内联重发)
  • 风险:低
  • 验证:调 send-verify → 响应无 code 字段;邮件仍能收到验证码

P1 安全加固 + 数据层统一(依赖顺序:11 → 12,其余独立)

P1-6 上传白名单 + /uploads 防护头(消除 R1

  • routes/upload.jsshaFileUpload 内校验 path.extname(...).toLowerCase()ALLOWED_EXT = ['.png','.jpg','.jpeg','.gif','.webp','.pdf','.zip','.txt','.md'] 内,否则报"不支持的文件类型";图片额外校验 file.mimetypeimage/ 开头
  • 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(服务端),renderSSRmarked.parse 结果过 DOMPurify.sanitizessrPagetitle/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.jslogin/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.jsadminOnly 增加 db.get('SELECT role FROM users WHERE id=?') 复查(M2
  • routes/blog.js?all=1authMiddleware, adminOnlyGET /posts/:id 未发布仅 admin/作者可见(M3)
  • 错误信息收敛:routes/forum.js:21,35server.js:163routes/email.js:54,93 对外统一 { error: '操作失败' },详情仅 console.errorM4
  • routes/email.js:17auth.js:93profile.js:25proxy.js:22rejectUnauthorized:false 收敛为配置项(默认关闭校验仅当 SMTP 自签时开启)(M5)
  • public/js/passwords.js:113forum.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.jsavatar 仅接受 /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.jsinitTables() 收敛——"确保列存在"函数(幂等补列,逻辑同现有 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:20node:22
  • 依赖:npm install better-sqlite3(v13,预编译随包,宝塔 glibc x64 免编译)
  • db.jsinitSqlJs() 异步初始化 → new Database(DB_PATH) 同步;run/get/all 重写:
    • rundb.prepare(sql).run(...params) + info.lastInsertRowid
    • getstmt.get(...params)allstmt.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/passwordsqlite3 data/rainweb.db "PRAGMA integrity_check;"

P2-16 死代码 + 前端重复清理

  • 删除 routes/links.js(引用不存在的 links 表,挂载即崩)、public/js/main.js(无引用)、public/js/passwords.js 内重复的 deleteFromDetail
  • S3-1register.html:85:92 重复调用 CAPTCHA.checkRequired('register'),删一行
  • S3-2escapeHtml/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 版本改读 VERSIONpackage.json version 与 VERSION 同步
  • README.mdNode 要求改 >= 22 LTS;删除更新/部署章节中 GitHub 引用
  • AGENTS.md:数据层章节重写(better-sqlite3、无 saveDb、单文件直写、cli.js 已统一)
  • 风险:低;验证:node cli.js help/api/version 显示一致

P2-18 可选加固(H 系列,按需确认)

  • H1helmet() + 收紧 CSPCDN 引入 marked 的 5 个页面加 SRI)——建议做
  • H3:验证码 Math.random()crypto.randomIntauth.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 前任何时刻可回滚)。