15 KiB
15 KiB
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. 没被提到、但必须一起考虑的
安全
- XSS:正文是 Markdown,现有渲染器不处理 HTML;编辑后必须转义/白名单,否则登录用户可自注入脚本(虽然是本地单用户,仍要防)。
- CSRF:Cookie 会话必须配 token 或
SameSite=Strict。 - 上传:封面/配图上传的存储路径、大小限制、MIME 校验、防目录穿越(public 是静态托管目录)。
- 密码:哈希算法与参数、首次设置流程、重置命令。
- 会话:过期、撤销、记住我、并发登录。
- CSP / 安全头:内嵌页面与 iconfont 的 CSP 白名单。
数据与迁移
- 迁移机制:现在没有迁移,
user_version只是标记;加表必须写迁移 + 升级测试。 - 默认值与校验:设置 JSON 的 schema 校验、缺字段回退默认、坏 JSON 不能白屏。
- 备份/恢复:编辑前自动备份(
stash.db.bak)、一键导出/导入。 - 审计:谁改了什么(单用户也需要排查)。
- 乐观锁/冲突:多标签页同时编辑。
- 草稿/发布:编辑后立即生效,还是保存草稿再发布?(内容库现在没有状态字段)
- 撤销:误删条目能否恢复(软删除 + 回收站)。
编辑体验
- 未保存离开:Drawer 关闭前的确认(beforeunload / 二次确认)。
- 表单校验:必填、长度、URL 合法性、错误定位。
- 图片上传:拖拽、进度、失败重试。
- 排序交互:拖拽排序(新组件),键盘可达的排序替代方案。
- 实时预览:Markdown 编辑预览(复用
ArticleBody)。 - 移动端编辑:Drawer 全宽、软键盘遮挡、保存按钮吸底。
- 可访问性:Drawer 焦点陷阱(已有
<dialog>)、表单 label/错误关联、快捷键。 - 编辑模式提示:登录后页面出现明显「编辑中」状态,避免误操作。
工程
- SSE 推送:编辑后前端已有
live通道,需要定义「设置变更」是否也推送。 - 缓存:设置读取缓存与失效策略。
- 并发写 SQLite:WAL、事务边界、写冲突重试。
- 测试:登录/权限/迁移/设置校验/编辑接口的单测 + e2e。
- 权限边界:未来多用户时,哪些接口要按角色收敛(现在先留
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. 开放决策(需拍板)
- 设置入口:独立
/admin页面(推荐)/ 右侧设置抽屉 / 两者都有。 - 登录范围:单用户密码(推荐)/ 仅局域网免密 + 编辑口令 / 多用户。
- 读接口是否也要登录:公开(推荐)/ 全站需登录。
- 内容编辑是否要「草稿 → 发布」两态:先不要(推荐,保持即时生效)/ 要。
- 编辑图片:先只接受 URL(推荐,P2 后期再做上传)/ 首期就要上传。
11. 实施记录
P8(2026-10-30,子分组开关)
- 与类型解耦:分区新增
allow_groups开关(迁移 v18);三种类型共用同一模板,开启后可建分组,关闭后平铺; - 默认值:老库升级时应用类开启、海报/图文关闭;已有分组的非应用分区自动开启;新建分区按类型给默认值,之后可随时改;
- 平铺兼容:关闭开关时读取会把(历史)分组内条目一起平铺,内容不丢;首页卡片若指向分组会自动挪回分区;
- 后台:「分区」每行新增「子分组」开关,拨动即保存并立刻出现/收起分组管理;「内容」弹窗的分组选择随开关出现;「侧栏导航」的建组入口同理;空分组不在侧栏显示;
- 验证:推荐电影(海报)开启开关 → 建组 → 加海报 → 详情按组显示;关闭 → 恢复平铺且条目仍在;
cargo test59 passed。
P7(2026-10-30,统一内容模型)
- 模型统一:分区(section)是容器,分组(group)是任意类型的可选子容器,条目可挂在分区或分组上;
view_mode只决定渲染方式; - catalog:所有类型都输出
groups(带条目)与items(直属分区条目); - 渲染:详情页三种类型都支持分组(多组显示小标题,直属条目进「未分组」);侧栏子菜单、首页卡片预览分组对所有类型生效;
- 后台:「分区」「侧栏导航」对所有类型可建组;「内容」弹窗的分组选择新增「直属分区」,编辑可换组;
- 修复:首页卡片配置为「整个分区」时应用类预览为空;catalog 条目查询改为按节点取,避免分组重复/错漏;
- 测试:59 passed。
P6(2026-10-30)
- 后台新增内容:
POST /api/items(key+nodeKey+ 条目字段)、DELETE /api/items/{key};「内容」Tab 用弹窗新增,列表逐条编辑(含换分区/分组)与删除; - 分组 CRUD:
POST /api/categories/{id}/groups、PATCH /api/groups/{id}(改名/侧栏显隐)、DELETE /api/groups/{id}(级联组内条目);「分区」Tab 内联管理,「侧栏导航」Tab 也可直接新建子分组; - 卡片预览分组:
PATCH /api/categories/{id}增加blockNodeKey,可在「分区」Tab 选择首页卡片预览哪个分组; - 后端校验:应用类条目必须挂在分组上;条目移动通过
PATCH /api/items/{key}的nodeKey; - 测试:59 passed(新增条目创建/移动/删除 3 例、分组 2 例)。
P5(2026-10-30)
- 跳转修复:保存条目时规范化
href(www.zhihu.com→https://www.zhihu.com,站内路径保留,javascript:/data:等协议拒绝),前端渲染再做一次兜底; - 侧栏子分类:
settings.sidebar增加hidden_groups/shown_groups/group_order,侧栏 Tab 展示分组并支持单独显隐、上下移; - 归类页:新增
PATCH /api/categories/{id},/admin的「归类」Tab 可改分区类型(应用/海报/图文)、标题、图标、色条、预览条数;切换类型时自动迁移条目与首页卡片指向; - 测试:54 passed(新增 href 规范化 3 例、分类更新 3 例)。
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:修改密码(成功后所有会话失效)、下载备份;
- 未做:恢复/导入(需要停机替换文件或在线校验导入流程)、回收站/软删除、图片上传、拖拽排序(当前用上移/下移按钮替代)。