diff --git a/docs/ADMIN-PLAN.md b/docs/ADMIN-PLAN.md new file mode 100644 index 0000000..174d594 --- /dev/null +++ b/docs/ADMIN-PLAN.md @@ -0,0 +1,146 @@ +# 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. 数据模型 + +```sql +-- 设置:一个 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 白名单。 + +### 数据与迁移 +7. **迁移机制**:现在没有迁移,`user_version` 只是标记;加表必须写迁移 + 升级测试。 +8. **默认值与校验**:设置 JSON 的 schema 校验、缺字段回退默认、坏 JSON 不能白屏。 +9. **备份/恢复**:编辑前自动备份(`stash.db.bak`)、一键导出/导入。 +10. **审计**:谁改了什么(单用户也需要排查)。 +11. **乐观锁/冲突**:多标签页同时编辑。 +12. **草稿/发布**:编辑后立即生效,还是保存草稿再发布?(内容库现在没有状态字段) +13. **撤销**:误删条目能否恢复(软删除 + 回收站)。 + +### 编辑体验 +14. **未保存离开**:Drawer 关闭前的确认(beforeunload / 二次确认)。 +15. **表单校验**:必填、长度、URL 合法性、错误定位。 +16. **图片上传**:拖拽、进度、失败重试。 +17. **排序交互**:拖拽排序(新组件),键盘可达的排序替代方案。 +18. **实时预览**:Markdown 编辑预览(复用 `ArticleBody`)。 +19. **移动端编辑**:Drawer 全宽、软键盘遮挡、保存按钮吸底。 +20. **可访问性**:Drawer 焦点陷阱(已有 ``)、表单 label/错误关联、快捷键。 +21. **编辑模式提示**:登录后页面出现明显「编辑中」状态,避免误操作。 + +### 工程 +22. **SSE 推送**:编辑后前端已有 `live` 通道,需要定义「设置变更」是否也推送。 +23. **缓存**:设置读取缓存与失效策略。 +24. **并发写 SQLite**:WAL、事务边界、写冲突重试。 +25. **测试**:登录/权限/迁移/设置校验/编辑接口的单测 + e2e。 +26. **权限边界**:未来多用户时,哪些接口要按角色收敛(现在先留 `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 后期再做上传)/ 首期就要上传。