Files
stash/docs/ADMIN-PLAN.md
T
stash dea7781b86 feat(auth): P0 登录与会话(迁移 v16、argon2id、HttpOnly Cookie)
- 迁移机制:schema::migrate 支持 15→16,新增 admin/session/setting 表
- 接口:session/setup/login/logout/password;SameSite=Strict + Origin 校验 + 失败限流
- 会话只存 token 的 sha256;改密清空全部会话
- 前端:/login 独立壳(首次=设置密码),页头登录/退出切换
- 测试:37 passed(迁移 + 登录/会话 4 例)
- 文档:README 登录说明、docs/ADMIN-PLAN.md 方案与 P0 记录
2026-10-29 12:41:03 +08:00

8.7 KiB
Raw Blame History

Stash 后台与编辑能力方案(讨论稿)

目标:登录后能改「左侧布局」「分区布局」「页头滚动信息」,并且能编辑内容。 状态:待评审,评审通过后按 §8 分期实现。

1. 结论先行

问题 建议 理由
要不要单独的设置页? 要,/admin,只放「全局配置」 布局、页头、侧栏是低频、结构化、跨页面的配置,塞进当前页会让页面变编辑器
内容(条目/文章)在哪编辑 就地编辑:详情页的 [编辑] 打开右侧 Drawer 编辑需要上下文预览;跳设置页会丢上下文
本地服务面板纳入设置吗 只纳入「显隐/位置」,数据本身不进设置 项目数、连接状态是运行时只读状态,不是可编辑配置
详情页要不要独立设置页 不要 内容编辑用 Drawer,全局配置才进 /admin

一句话:全局配置进 /admin,单个内容就地改。

2. 登录方案(单用户、本地)

  • 账号:单管理员(admin),密码用 argon2id 哈希存库;首次启动若无密码,生成随机密码打印到控制台 / 或首访引导设置。
  • 会话:登录成功下发 HttpOnly + SameSite=Lax + Secure(可选) 的 Cookie(随机 256bit token,库里存哈希,7 天过期,可撤销)。
  • 写操作:所有 POST/PATCH/DELETE 校验会话 + CSRF token(双提交 Cookie 或自定义头)。
  • 读操作:页面与 GET /api/* 保持公开(本地工具,保持只读可用);写接口一律要求登录。
  • 频控:登录失败 5 次锁 1 分钟,防止本地暴破。
  • 不做的:多用户/角色、OAuth、找回密码(单用户场景用「重置密码」命令)。

3. 信息架构:/admin

Tab 内容 存储
布局 侧栏项顺序/显隐/分组、分区卡片顺序、每卡展示的分组与条数 settings.layout
页头 滚动信息条目(文本/标签/强调色)、速度、是否启用;品牌名 settings.header
内容 分区、分组、条目的增删改与排序(批量) 现有内容库表
系统 修改密码、本地服务面板显隐、备份/导出、恢复默认 settings.system / auth

侧栏与页头的实时预览:设置页右侧放一个缩小的 Layout 预览(复用组件库)。

4. 编辑模型

4.1 内容编辑(就地)

  • 详情页 [编辑] → Drawer(宽 480)表单:标题、标签、描述、正文(Markdown)、封面、链接;
  • 保存:PATCH /api/items/:id,乐观锁(updatedAt),冲突时提示「内容已被修改,刷新后重试」;
  • 成功:toast.success + 局部刷新当前条目(不整页刷新)。

4.2 全局配置(/admin)

  • 表单保存走 PATCH /api/settings,整块 JSON 校验后落库;
  • 提供「恢复默认」与「导出 JSON / 导入」。

5. 数据模型

-- 设置:一个 key 一段 JSON,带版本号用于乐观锁
settings(key TEXT PRIMARY KEY, value TEXT NOT NULL, version INTEGER NOT NULL, updated_at TEXT);

-- 管理员(单行)
admin(id INTEGER PRIMARY KEY CHECK (id = 1), username TEXT, password_hash TEXT, updated_at TEXT);

-- 会话
sessions(token_hash TEXT PRIMARY KEY, created_at TEXT, expires_at TEXT, last_seen_at TEXT);

-- 审计(谁在什么时候改了什么)
audit(id INTEGER PRIMARY KEY AUTOINCREMENT, at TEXT, action TEXT, target TEXT, detail TEXT);

迁移问题:当前 PRAGMA user_version = 15 且「删库重建」。加这些表要引入第一套迁移(v15 → v16),否则老库升级会坏。这是必须先解决的前置项。

6. API 草案

POST   /api/auth/login      {username,password} -> set-cookie
POST   /api/auth/logout
GET    /api/auth/session    -> {authenticated, username}
PATCH  /api/auth/password   {current,next}

GET    /api/settings        -> {layout, header, system, version}
PATCH  /api/settings        {partial} + If-Match: version

PATCH  /api/items/:id       条目编辑(乐观锁)
POST   /api/categories      新建分区
PATCH  /api/categories/:id  分区配置(展示块、预览条数、色条)
DELETE /api/items/:id

7. 没被提到、但必须一起考虑的

安全

  1. XSS:正文是 Markdown,现有渲染器不处理 HTML;编辑后必须转义/白名单,否则登录用户可自注入脚本(虽然是本地单用户,仍要防)。
  2. CSRF:Cookie 会话必须配 token 或 SameSite=Strict。
  3. 上传:封面/配图上传的存储路径、大小限制、MIME 校验、防目录穿越(public 是静态托管目录)。
  4. 密码:哈希算法与参数、首次设置流程、重置命令。
  5. 会话:过期、撤销、记住我、并发登录。
  6. CSP / 安全头:内嵌页面与 iconfont 的 CSP 白名单。

数据与迁移

  1. 迁移机制:现在没有迁移,user_version 只是标记;加表必须写迁移 + 升级测试。
  2. 默认值与校验:设置 JSON 的 schema 校验、缺字段回退默认、坏 JSON 不能白屏。
  3. 备份/恢复:编辑前自动备份(stash.db.bak)、一键导出/导入。
  4. 审计:谁改了什么(单用户也需要排查)。
  5. 乐观锁/冲突:多标签页同时编辑。
  6. 草稿/发布:编辑后立即生效,还是保存草稿再发布?(内容库现在没有状态字段)
  7. 撤销:误删条目能否恢复(软删除 + 回收站)。

编辑体验

  1. 未保存离开:Drawer 关闭前的确认(beforeunload / 二次确认)。
  2. 表单校验:必填、长度、URL 合法性、错误定位。
  3. 图片上传:拖拽、进度、失败重试。
  4. 排序交互:拖拽排序(新组件),键盘可达的排序替代方案。
  5. 实时预览:Markdown 编辑预览(复用 ArticleBody)。
  6. 移动端编辑:Drawer 全宽、软键盘遮挡、保存按钮吸底。
  7. 可访问性:Drawer 焦点陷阱(已有 <dialog>)、表单 label/错误关联、快捷键。
  8. 编辑模式提示:登录后页面出现明显「编辑中」状态,避免误操作。

工程

  1. SSE 推送:编辑后前端已有 live 通道,需要定义「设置变更」是否也推送。
  2. 缓存:设置读取缓存与失效策略。
  3. 并发写 SQLite:WAL、事务边界、写冲突重试。
  4. 测试:登录/权限/迁移/设置校验/编辑接口的单测 + e2e。
  5. 权限边界:未来多用户时,哪些接口要按角色收敛(现在先留 require_auth 中间件层)。

8. 分期建议

阶段 范围 依赖
P0 迁移机制 v16 + admin/sessions/settings 表 + 登录/登出/会话 + 登录页 + 写接口鉴权 无
P1 /admin 设置页:页头滚动信息、侧栏项显隐/顺序、分区卡配置 P0
P2 内容就地编辑(Drawer 表单 + 乐观锁 + 富文本/图片上传) P0 + 迁移
P3 备份/恢复、审计、回收站、导入导出 P1/P2

9. 组件库缺口(P1/P2 会用到)

缺口 说明
Textarea 多行输入(Markdown、描述),当前只有单行 Input
ColorPicker 分区色条 / 标签色
SortableList 拖拽排序 + 键盘上/下移
表单布局 FormField 有了,但「两列/分组/折叠」还需要页面组合或补 Form
Tabs/Drawer/Switch/Toast 已有,可直接用

10. 开放决策(需拍板)

  1. 设置入口:独立 /admin 页面(推荐)/ 右侧设置抽屉 / 两者都有。
  2. 登录范围:单用户密码(推荐)/ 仅局域网免密 + 编辑口令 / 多用户。
  3. 读接口是否也要登录:公开(推荐)/ 全站需登录。
  4. 内容编辑是否要「草稿 → 发布」两态:先不要(推荐,保持即时生效)/ 要。
  5. 编辑图片:先只接受 URL(推荐,P2 后期再做上传)/ 首期就要上传。

11. 实施记录

P0(已完成,2026-10-29)

  • 迁移机制:user_version 15 → 16,schema::migrate 逐级升级;v15 老库启动时自动补 admin / session / setting 三表;
  • 登录:POST /api/auth/setup(仅首次)、/login、/logout、PATCH /api/auth/password;argon2id + sha256(token) 落库;
  • 会话:HttpOnly + SameSite=Strict + Path=/,7 天;改密后所有会话失效;
  • 防护:Origin 同源校验、连续失败 5 次锁 60 秒;
  • 前端:/login 独立壳(入口按 pathname 分流)、页头登录/退出登录切换;
  • 测试:后端 37 个单测(新增迁移 2 个 + 登录 4 个),登录流程 e2e 手动脚本验证通过。

未做(P1/P2):/admin 设置页、侧栏/页头编辑、内容就编辑、setting 表的读写接口。