Files
stash/docs/ADMIN-PLAN.md
T
stash 427338b329 feat(admin): 分区增删、资料设置、操作条与交互收尾
- POST/DELETE /api/categories(需登录,级联,local 保护)
- profile 设置(头像/名称/角色/简介/位置)应用到资料卡
- 详情操作条:浏览 +1、点赞切换、分享复制、跳转外链
- 编辑按钮垂直居中且留 12px;设置页按钮上间距、保存 Toast、Tabs 去滚动条
- 登录密码可见切换
- 测试 48 passed
2026-10-29 20:15:56 +08:00

207 lines
12 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.
# 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 焦点陷阱(已有 `<dialog>`)、表单 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 后期再做上传)/ 首期就要上传。
## 11. 实施记录
### P4(2026-10-29,收尾)
- 分区增删:`POST /api/categories`、`DELETE /api/categories/{id}`(需登录,级联内容,`local` 不可删),首页与侧栏共用;
- 资料设置:头像 URL / 名称 / 角色 / 简介 / 位置,应用到侧栏资料卡;
- 操作条:进入详情页记一次浏览、点赞可切换(localStorage 记状态)、分享复制链接、跳转外链;
- 交互细节:`[编辑]` 与标题垂直居中并留 12px 间距;设置页操作按钮与内容拉开间距;保存改用 Toast(2.4s 自动消失);设置页 Tabs 不再出现滚动条;
- 登录密码框增加显示/隐藏切换(错误后仍可用);
- 测试:48 passed(新增 categories 2 例、settings 资料校验)。
### 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` 表的读写接口。
### P1(已完成,2026-10-29)
- 登录改为**页内弹窗**(`LoginDialog`):`/login` 旧地址重定向回首页,不再单开页面;
- 设置读写:`GET /api/settings`(公开)、`PATCH /api/settings`(需登录,`version` 乐观锁,冲突 409、校验失败 400);
- `setting` 表存 `key='site'` 的 JSON:页头(启用 / 速度 / 条目与标签/色调)、侧栏(隐藏 / 顺序);
- `/admin` 设置页:Tabs(页头滚动信息 / 侧栏导航),条目支持增删、上移下移、占位符 `{count}`/`{project}`/`{status}`;
- 首页应用设置:滚动条目的文本/标签/速度/启用,侧栏按顺序与显隐渲染;
- 测试:42 passed(新增 settings 5 例:默认值、版本递增、冲突、校验、坏 JSON 回落)。
### P1 扩展(2026-10-29,同日)
- 首页卡片可配置:显隐、顺序、标题覆盖、每卡预览条数(`settings.sections`,前端 `HomeView` 应用);
- 页头滚动信息支持**启用/禁用**(`header.enabled`,关闭后页头中间留空);
- 「编辑」按钮只在登录后出现(`canEdit`,未登录时详情页与文章页不渲染 `[编辑]`);
- 侧栏「首页」与卡片顺序相互独立:侧栏管导航,卡片管首页布局。
### P2(已完成,2026-10-29)
- 库新增 `Textarea`(stash-ui v0.2.0):`rows` / `resize` / `invalid`;
- 条目编辑:`GET /api/items/{key}`(公开读)、`PATCH /api/items/{key}`(需登录);
- 乐观锁:`entry.updated_at`(迁移 v17),冲突返回 409;
- 可编辑字段:标题、简介、标签、一句话评价、正文 Markdown、封面、跳转地址;
- 前端:详情页 `[编辑]` → `Drawer` 表单;保存后 `catalog.reload()` 局部刷新,不整页刷新;
- 审计:每次条目更新写入 `audit` 表;
- 测试:46 passed(新增 entries 4 例:更新/标签替换、冲突、空标题、不存在)。
### P3(部分完成,2026-10-29)
- 审计表 `audit`:记录 `item.update`、`settings.update`;
- 备份:`GET /api/backup`(需登录)用 `VACUUM INTO` 生成一致性副本并以附件下载;
- 系统设置 Tab:修改密码(成功后所有会话失效)、下载备份;
- **未做**:恢复/导入(需要停机替换文件或在线校验导入流程)、回收站/软删除、图片上传、拖拽排序(当前用上移/下移按钮替代)。