整理文档

This commit is contained in:
ChuNa7a committed 2026-10-16 18:09:11 +08:00
1 parent 06a3d8506b
commit 0e347aad9e
26 files changed
+1552 -521

No files matched your search

+46 -8
View File
@@ -1,12 +1,21 @@
# 后端 API
全部正文使用 JSON。同源 `Origin` 必须等于 `PUBLIC_ORIGIN`,例如 `https://blog.example.com`。响应含 `Cache-Control: no-store`。登录 Cookie 为 `HttpOnly; SameSite=Strict; Secure`,仅 localhost HTTP 开发场景省略 Secure。
[返回项目首页](../README.md)
## 请求约定
- 全部正文使用 JSON。
- 同源 `Origin` 必须等于 `PUBLIC_ORIGIN`,例如 `https://blog.example.com`。
- 响应含 `Cache-Control: no-store`。
- 登录 Cookie 为 `HttpOnly; SameSite=Strict; Secure`,仅 localhost HTTP 开发场景省略 Secure。
## 接口一览
| 方法 | 路径 | 权限 / 请求 |
| --- | --- | --- |
| GET | `/api/health` | 公共,API 存活,不代表数据库或发布同步健康 |
| GET | `/api/posts` | 公共,`{commit,posts:[metadata]}`,仅已发布,日期倒序 |
| GET | `/api/posts/:slug` | 公共,`{meta,markdown,assets:[{name,url}]}`,草稿及不存在均 404 |
| GET | `/api/posts/:slug` | 公共,`{meta,markdown,format,author,article_id,assets:[{name,url}]}`,草稿及不存在均 404 |
| GET | `/api/assets/:slug/:name` | 公共,仅已发布文章的已验证图片 |
| GET | `/api/stats` | 公共,文章数、标签数、正文源字节数、启动以来文章请求数 |
| POST | `/api/auth/register` | 公共 + Origin;`{username,password}`,创建 reader;可配置关闭 |
@@ -16,15 +25,44 @@
| PUT | `/api/account/ssh-key` | 登录 + Origin + CSRF;`{public_key}`,替换自己的唯一 Ed25519 或 RSA 公钥(RSA 2048–8192 位) |
| PUT | `/api/account/password` | 登录 + Origin + CSRF;`{old_password,new_password}`,成功后全部会话失效 |
| GET | `/api/admin/users` | 管理员;返回用户启用状态、角色、前缀及公钥,不含哈希 |
| PATCH | `/api/admin/users/:username` | 管理员 + Origin + CSRF;`{enabled,role,prefixes}` |
| PATCH | `/api/admin/users/:username` | 管理员 + Origin + CSRF;`{enabled,role}`;兼容可选的旧 `prefixes` 字段 |
| GET | `/api/admin/status` | 管理员;`{commit,total_posts,drafts,indexed_commit}` |
角色:`reader | publisher | admin`。网站管理员与数据库账号、OS 用户不是同一个系统。发布角色仍需已登记公钥和非空文章前缀才能使用 SSH。网站登录不产生 Git 访问凭证。
## 权限与账户
用户名:3–32 位小写字母、数字、短横线,以字母开头;口令:12–1024 字节;前缀:2–64 位小写英文、数字、短横线且以短横线结尾,最多 8 个。注册不接受 `role` 等额外字段。公钥不能与别的用户重复。
- 角色:`reader | publisher | admin`。
- 网站管理员与数据库账号、OS 用户不是同一个系统。
- 发布角色仍需已登记公钥才能使用 SSH。
- 每个账户使用自己的仓库,旧前缀字段不再承担授权作用。
- 网站登录不产生 Git 访问凭证。
修改请求的头为 `Origin: https://blog.example.com` 和 `X-CSRF-Token: <登录返回的 csrf>`,并携带 Cookie。没有配置跨域 CORS,也没有浏览器本地持久存储令牌的要求。授权或停用用户会使该用户既有会话失效;再次登录后才能继续使用自己的账户接口。
用户名:3–32 位小写字母、数字、短横线,以字母开头;口令:12–1024 字节。旧前缀字段仅兼容接收,若传入仍按原格式校验。注册不接受 `role` 等额外字段。公钥不能与别的用户重复。
普通业务错误返回 `{ "error": "可读说明" }`;401 未登录/口令错误,403 Origin/CSRF/权限错误,404 不存在或非公开内容,409 用户名冲突,429 超过限流。Axum 的无效 JSON/字段结构错误使用框架的 400/415/422 响应,超过请求体上限返回 413。服务器内部错误不返回路径、凭据或数据库详情。
## 修改请求
文章写入、上传、在线预览、后台发布/删除接口当前都不存在。文章修改和权限审批分开:管理员只能通过账号 API 批准身份与命名空间;文章在作者本机编辑,通过 SSH Git 发布。
- 修改请求的头为 `Origin: https://blog.example.com` 和 `X-CSRF-Token: <登录返回的 csrf>`,并携带 Cookie。
- 没有配置跨域 CORS,也没有浏览器本地持久存储令牌的要求。
- 授权或停用用户会使该用户既有会话失效;再次登录后才能继续使用自己的账户接口。
## 错误响应
- 普通业务错误返回 `{ "error": "可读说明" }`;401 未登录/口令错误,403 Origin/CSRF/权限错误,404 不存在或非公开内容,409 用户名冲突,429 超过限流。
- Axum 的无效 JSON/字段结构错误使用框架的 400/415/422 响应,超过请求体上限返回 413。
- 服务器内部错误不返回路径、凭据或数据库详情。
## 文章发布边界
文章写入、上传、在线预览、后台发布/删除接口当前都不存在。文章修改和权限审批分开:管理员只能通过账号 API 批准身份与发布资格;文章在作者本机编辑,通过 SSH Git 发布。
## 文章与资源
- 文章以作者仓库的 `index.txt` 为登记依据,每行三列:`文章ID posts/文件夹 正文文件名`。
- 站内 `:slug` 为 `作者名~文章ID`,例如 `alice~first`;索引 ID 不随文件夹或文件名变化。
- `markdown` 保留现有正文源字段(含 Frontmatter);`format` 为 `markdown` 或 `text`,纯文本不得当作 HTML 渲染。
- 未登记内容及草稿不通过公共文章/资源接口返回;删除索引项会自动撤回阅读内容并同步删除派生数据库记录。
## 同步状态
- `/api/admin/status` 的 `commit` 和 `indexed_commit` 为整个作者集合的版本摘要,不是单个作者的 Git HEAD。
- 作者各自的源提交记录在数据库 `blog_articles.git_commit` 和私有集合快照中。
- 数据库失败时 Git 接收与阅读仍可继续,索引在恢复后自动重建;站长审核暂未启用。
+4 -1
View File
@@ -6,7 +6,10 @@
## 决定
当前只做 Rust/Axum 后端。网站账户注册后为 reader;公钥登记和管理员发布授权分别完成。OpenSSH 使用固定 `AuthorizedKeysCommand` 动态读取独立授权文件,为每把允许的公钥返回绑定站内用户名的强制命令。OS 只有一个无交互权限的 git 账户,网站用户不能自行决定命令或仓库路径。
- 当前只做 Rust/Axum 后端。
- 网站账户注册后为 reader;公钥登记和管理员发布授权分别完成。
- OpenSSH 使用固定 `AuthorizedKeysCommand` 动态读取独立授权文件,为每把允许的公钥返回绑定站内用户名的强制命令。
- OS 只有一个无交互权限的 git 账户,网站用户不能自行决定命令或仓库路径。
接收仓库与正式内容仓库分属不同进程身份。pre-receive 在 Git quarantine 内验证全部候选文件、单提交历史和改动命名空间;API 再验证并导入独立正式仓库,从该 Git 生成已发布快照。ChuSQL 只保存可重建的公开文章元数据与标签,HTTP 读取不依赖数据库。
+4 -1
View File
@@ -8,7 +8,10 @@
数据库服务与 Rust 驱动作为两个独立 Git 子模块维护。主仓库只提交远程地址、固定版本指针、初始化/构建配置,不提交依赖源码或编译产物。Cargo 引用初始化后的驱动路径,数据库从外部 checkout 构建并暂存到被忽略的 Docker 构建目录。
子模块默认使用 `https://github.com/TheBadRoger/ChuSQL.git` 与 `https://github.com/TheBadRoger/ChuSQL-RustDriver.git`。初始化脚本在连接、访问或固定提交获取失败时,分别回退到 `https://repo.chunana.cn/ChuNa7a/ChuSQL.git` 与 `https://repo.chunana.cn/ChuNa7a/ChuSQL-RustDriver.git`。所有远程来源均使用 HTTPS,不提供 SSH 来源选项;空仓库留下的半初始化状态也可恢复。回退仍保持固定提交,不自动跟随远程最新分支。
- 子模块默认使用 `https://github.com/TheBadRoger/ChuSQL.git` 与 `https://github.com/TheBadRoger/ChuSQL-RustDriver.git`。
- 初始化脚本在连接、访问或固定提交获取失败时,分别回退到 `https://repo.chunana.cn/ChuNa7a/ChuSQL.git` 与 `https://repo.chunana.cn/ChuNa7a/ChuSQL-RustDriver.git`。
- 所有远程来源均使用 HTTPS,不提供 SSH 来源选项;空仓库留下的半初始化状态也可恢复。
- 回退仍保持固定提交,不自动跟随远程最新分支。
服务器上的 Git 地址覆盖、操作系统账户和 SSH 配置由服务器自己管理,项目不配置 localhost 映射或固定 deploy 用户。Docker 构建前使用同一初始化脚本,继承运行者现有 Git 配置。开发者仍可以显式使用已有文件路径,此时文件协议仅对该次初始化命令启用,不设置全局许可。
@@ -0,0 +1,38 @@
# ADR-003:按作者隔离仓库,以索引登记文章
## 状态与日期
已接受,2026-10-16。替代 ADR-001 中共享仓库、slug 前缀隔离与固定正文路径的决定;唯一 Git 写入口、SSH 双重授权和账户存储现状不变。
## 决定
- 每位作者拥有 `/srv/ingress/authors/<用户名>.git`,API 在私有数据卷按作者分别保存 Git 缓存。
- 客户端仍只使用 `content.git` 别名,SSH 强制命令按服务器公钥绑定的身份选定真实仓库,不接受客户端指定路径。
- Git 全局信任范围仅限受控 `/srv/ingress/authors/*` 目录,以支持跨 UID 本地读取,不信任其他目录。
- 仓库首次访问时自动创建,作者无须服务器 shell 或手动建裸仓库。
- 不同仓库不共享对象库或历史。
- 根目录 `index.txt` 使用 UTF-8 三列文本,每行 `文章ID posts/文件夹 正文文件名`,空格或 Tab 分隔,无表头,允许空行。
- 每作者 ID 唯一、文件夹不重复,正文为指定的 txt/md/markdown 文件,元信息保留 YAML Frontmatter,slug 可省略。
- 站内 ID 为 `用户名~文章ID`,两部分均不能含 `~`,避免重名与歧义;目录或正文改名不改变身份。
- pre-receive 在 Git quarantine 中检查完整候选树、单个线性提交、索引引用、白名单、文件模式、文本和图片内容与配额。
- 接收层能够拒绝非法文件,故整次拒绝非法推送,不静默过滤后制造与作者提交不同的服务端分支。
- 未登记目录和未选中的正文不导入阅读内容及数据库,但仍受校验和存储配额限制,不能作为隐藏载荷通道。
- 只作为资源导入所选正文引用的已验证图片。
- API 再次验证各作者候选内容,构建私有集合快照及公开阅读快照。
- 数据库 `blog_articles` 保存有效登记的作者、本地 ID、正文路径、格式、元信息和源提交;公开检索继续使用 `blog_posts/blog_tags`。
- 索引条目移除后撤回正文和图片入口,并在数据库可用时事务重建派生记录。
- 数据库失败不阻止已导入文章阅读,恢复后自动对账重建。
- 集合版本是作者提交映射的摘要,单个作者 Git 提交仍单独保存。
## 取舍
索引是发布登记依据,不是远程代码执行配置。路径和文件名不允许空格、隐藏段或任意嵌套,暂不支持 SVG、HTML、脚本、配置或证书文件。文本中的 PEM 标记直接拒绝,包括代码块;这有意比一般技术博客严格,不能据此保证识别任意编码或伪装的秘密。
单作者 Git 历史仍保留自己的已删内容,移除索引不会释放历史占用;硬磁盘配额和管理员离线维护仍必要。站点总导入配额可能使通过单作者检查的推送等待处理,Git 接收不等于内容已导入或数据库已同步。当前按完整集合重建派生表,未来账户、互动、审核和站长管理状态必须用独立持久表,不能随着索引重建丢失。
旧共享仓库需要停机备份、按所有权导出最新文件、建立无共享历史的个人仓库,并重新生成索引。不会自动猜测所有权或清除旧数据;迁移方式见[运维与迁移](../operations.md)。已经分发的旧历史无法收回,旧 URL 的重定向尚未实现。
站长审核仅列入 TODO;本轮校验通过且状态为 published 的登记文章自动公开。后续应在索引导入与公开发布之间加入审核,审核未通过的修改不能替换原已通过版本。
+11
View File
@@ -0,0 +1,11 @@
# 架构决策
[返回项目首页](../../README.md)
| 决策 | 内容与适用范围 |
| --- | --- |
| [ADR-001](0001-git-only-publishing.md) | Git 唯一写入口与身份隔离;共享仓库方案已由 ADR-003 替代 |
| [ADR-002](0002-external-dependencies.md) | 外部依赖子模块、固定提交与 HTTPS 回退 |
| [ADR-003](0003-author-repositories-and-manifest.md) | 作者独立仓库与文章清单 |
旧决策保留其历史背景;当前作者隔离与文章索引规则以 ADR-003 为准。
+53
View File
@@ -0,0 +1,53 @@
# 部署
[返回项目首页](../README.md)
## 构建与启动
- 部署基于 Debian 13。
- `scripts/deploy.sh` 会使用同一个依赖初始化脚本,自动按[依赖源顺序](development.md)进行 HTTPS 回退,再构建外部数据库与 Docker 镜像;沿用运行者现有的 Git 配置,不在项目内覆盖服务器环境。
```sh
scripts/deploy.sh
```
## 首次部署
先按[开发与依赖](development.md)初始化子模块,再执行以下同架构构建步骤。使用 `scripts/deploy.sh` 时,也须先准备 `.env`。
```sh
# ChuSQL 尚未编译时,先按其项目说明构建 Rust 存储库、server 与 bootstrap。
python3 scripts/prepare-deploy.py --build-database
cp .env.example .env
chmod 600 .env
# 从标准输入输入管理员口令,Ctrl-D 结束;不要把明文写进命令行历史。
./deploy/bin/chublog-api hash-password
# 将输出以单引号包裹写入 .env 的 ADMIN_PASSWORD_HASH;同时配置域名及两份不同的随机 DB 口令。
docker compose --env-file .env -f deploy/compose.yaml up -d --build
# db 启动就绪后,创建 chublog 库和非超级用户 chublog_index;失败时检查日志后重试。
docker compose --env-file .env -f deploy/compose.yaml run --rm db-init
```
部署入口启动服务后,首次仍需执行上面的 `db-init` 命令;入口不会在数据库启动竞态中静默吞掉初始化错误。
## 构建选项与平台
构建时若 Debian 官方下载源较慢,可在 `.env` 配置 `DEBIAN_MIRROR=mirrors.ustc.edu.cn`,仍使用 Debian 签名校验。
- `prepare-deploy.py` 默认从数据库子模块读取编译产物;`--build-database` 会先构建其 Rust 存储库、Haskell server 和 bootstrap。
- 也可用 `--chusql-source /已有构建的/ChuSQL` 选择已有外部构建,没有产物会明确失败。
- 编译产物只暂存在被忽略的 `deploy/bin/`。
- 跨平台部署须先在目标 Debian/架构构建,不能复制 macOS、Windows 或其他架构二进制。
- 镜像不含编译器、源码、`.env` 或密码文件。
## 管理员初始化
首次启动从环境中的哈希创建管理员,后续账号与权限以持久文件为准,修改初始哈希不会覆盖已有账户。
## 域名与网络
- Caddy 根据 `SITE_ADDRESS` 提供 HTTPS 和 API 反向代理;`PUBLIC_ORIGIN` 必须与外部站点完全一致,不带结尾斜杠。
- HTTP 仅对 localhost 开发场景允许。
- 只需公网 TCP 80/443 和 Git SSH 2222(可改);主机管理 SSH 另行管理,数据库和 API 不映射宿主端口。
- 既有 UDP 7001 不受影响。
- 参考 [Caddy 请求体限制](https://caddyserver.com/docs/caddyfile/directives/request_body) 与 [反向代理文档](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy)。
+74
View File
@@ -0,0 +1,74 @@
# 开发与依赖
[返回项目首页](../README.md)
```text
ChuBlog/
├── apps/api/
├── external/
│ ├── ChuSQL/ # Git 子模块,主仓库只记录版本指针
│ └── ChuSQL-RustDriver/ # Git 子模块
└── scripts/init-deps.py
```
## 获取代码
项目提供两个 HTTPS 仓库,任选其一克隆:
```sh
git clone https://github.com/TheBadRoger/ChuBlog.git
# 或:git clone https://repo.chunana.cn/ChuNa7a/ChuBlog.git
cd ChuBlog
python3 scripts/init-deps.py
```
## 依赖来源与回退
数据库和驱动均提供两个 HTTPS 源:
| 依赖 | GitHub | 自建仓库 |
| --- | --- | --- |
| ChuSQL | https://github.com/TheBadRoger/ChuSQL.git | https://repo.chunana.cn/ChuNa7a/ChuSQL.git |
| ChuSQL-RustDriver | https://github.com/TheBadRoger/ChuSQL-RustDriver.git | https://repo.chunana.cn/ChuNa7a/ChuSQL-RustDriver.git |
- 初始化先尝试 GitHub HTTPS,连接、访问或固定提交获取失败时回退自建 HTTPS,无需 SSH 身份。
- 已正确初始化的固定版本直接复用,有未提交修改时拒绝覆盖;回退不改为任意最新分支。
- 项目不设置全局 `insteadOf`、localhost 映射、OS 用户或服务器 SSH 配置。
## 使用本地依赖
开发电脑仍可显式选择已有本地来源(仅开发覆盖参数,不是服务器默认流程);只读取已提交版本:
```sh
python3 scripts/init-deps.py --database-repo ../ChuSQL --driver-repo ../ChuSQLDriver
```
- 子模块固定到已提交版本;源仓库必须包含这些提交。
- 依赖源码和构建产物不进入博客仓库,更新依赖时只提交 `.gitmodules`/配置及子模块指针。
- 不要对主仓库使用 `git add --force` 重新收录依赖文件。
## 开发检查
```sh
cargo test --all-targets
cargo fmt --check
cargo clippy --all-targets -- -D warnings
```
## 联调
```sh
python3 scripts/prepare-deploy.py --build-database
python3 scripts/smoke.py
python3 scripts/smoke.py --key-type rsa
```
- 联调使用独立随机命名的 Compose 项目、本机 18080/18443/12222 端口,测试结束清理自己创建的容器与卷。
- 覆盖真实 SSH 身份与授权、作者间 Git 对象隔离、非法推送、索引删除、ChuSQL 元信息创建/删除、发布撤回、断库阅读、重启与撤权。
- 该测试会拉取基础镜像,需要 Docker 可用。
## 历史清理后的协作
- 驱动的旧源码副本已从主仓库可达历史移除,后续只保留外部子模块配置。
- 历史重写后,已有克隆建议重新克隆并初始化依赖;不要把旧 main 合并回来,否则可能重新引入已删除的依赖历史。
- 自建服务器的不可达对象清理与其他人的旧克隆需要分别处理,GitHub 缓存和旧提交直链的回收由平台管理。
+48
View File
@@ -0,0 +1,48 @@
# 运维与迁移
[返回项目首页](../README.md)
## 持久化、故障和恢复
| 持久卷 | 内容 |
| --- | --- |
| `content` | 私有 Git 缓存 `authors/<用户名>/content.git`、`collection.json`、`users.json`、公开 `published.json` |
| `authorization` | 发布授权与公钥 |
| `ingress` | 作者接收仓库 `authors/<用户名>.git` |
| `ssh-host-keys` | SSH 服务器身份 |
| `database` / `db-logs` | 数据库数据 / 数据库日志 |
应用日志输出 stdout,由 Docker 分卷轮换,不记录口令、会话或私钥。
## 数据库故障与索引重建
- 数据库索引失败不会修改作者 Git 或阻塞已导入文章的 HTTP 阅读;新集合会触发同步,失败至少间隔 30 秒重试,并定期检查索引是否丢失。
- `blog_articles` 保存全部有效登记文章(含草稿)的作者、本地 ID、正文路径、格式、源提交和元信息;`blog_posts/blog_tags` 保留公开检索索引。
- 恢复数据库后可自动重建;离线命令为 `chublog-api rebuild-index`(必须停止同一数据卷上的 API 进程后运行,防止并发),或等待在线自动重试。
- `indexed_commit:null` 表示尚无已确认可用的索引,旧值表示尚未追平当前内容。
- 重建上述派生表使用 ChuSQL 事务;数据库不可用时保留阅读快照,恢复后重建。
## 备份与恢复
- 备份必须同时包含正式 Git、用户文件、授权卷、SSH 主机密钥;备份包含密码哈希,须加密并限制权限。
- 接收 Git 可额外镜像备份。
- 恢复时先停止写入,从同一备份恢复各卷,再启动;不要只恢复 ChuSQL,也不要直接向正式仓库推送。
- 应用启动加载私有集合快照,并继续从各作者 Git 校验导入。
- 丢失派生快照时,可在停机并保留备份后移走损坏的 `collection.json`,由各作者接收仓库重新导入;不要丢失作者仓库或账户授权。
- 进程重启会注销所有会话。
本实现对文件写入执行原子替换与 fsync,Git 引用采用受控更新;磁盘故障、主机断电和备份恢复仍需真实环境演练。不要对含正式数据的项目执行 `docker compose down -v`。
## 从旧共享仓库迁移
这是不兼容的存储与 URL 变更,不能直接把旧共享 Git 历史提供给新的个人仓库。先停机并加密备份全部卷,再完成以下操作:
1. 从旧共享仓库导出各作者最新的文章文件(含草稿),按原发布前缀及管理员确认的所有权分给作者;不要复制 `.git` 或旧提交历史。
2. 管理员离线将 `/auth/publishers.json` 的 `repository` 改为 `/srv/ingress/authors`(或自定义 `INGRESS_DIR`),保留用户和公钥授权。新版本 init 服务会创建该根目录。
3. 将旧 API 数据卷根目录的 `content.git` 移入只对管理员可见的备份位置;旧 `published.json` 同样备份。旧 SSH 共享仓库不再供服务使用,应移出接收卷离线保管。不要删除备份或清空用户数据。
4. 启动新服务。每位作者重新 clone 自己的空仓库,复制自己的导出文件,创建三列 `index.txt`,整理成一个首次提交并推送。旧 `slug` 如保留,须与索引 ID 一致。
5. 核对草稿隔离、所有文章、数据库索引和新的 `作者名~ID` URL 后再恢复对外服务。旧版本已被其他人克隆的内容无法收回。
- 已有授权配置仍指向旧仓库时,新版本拒绝启动;不会自动重新分发旧历史。
- 原链接到新 ID 的重定向尚未实现,迁移时需另行安排。
- 若自定义 `INGRESS_DIR`,需同步调整镜像内 `deploy/gitconfig` 的受信目录范围;默认仅信任 `/srv/ingress/authors/*`,不使用全局 `safe.directory=*`。
+107
View File
@@ -0,0 +1,107 @@
# 账户授权与文章发布
[返回项目首页](../README.md)
## 用户与发布授权
完整接口见 [API 文档](API.md)。
1. 注册:`POST /api/auth/register`,正文 `{ "username": "alice", "password": "至少12字节的口令" }`。默认角色 `reader`,绝不接受客户端指定角色。`ALLOW_REGISTRATION=false` 可关闭注册。
2. 登录:`POST /api/auth/login`,相同字段。浏览器保存 HttpOnly Cookie;返回 `csrf`。所有修改请求必须携带站点 `Origin` 和 `X-CSRF-Token`。
3. 本地生成 `ssh-keygen -t ed25519` 或 `ssh-keygen -t rsa -b 3072`;登录后 `PUT /api/account/ssh-key` 提交 `{ "public_key": "ssh-ed25519 AAAA..." }`,只交公钥。RSA 使用标准 `ssh-rsa AAAA...` 公钥文本,支持 2048–8192 位;实际认证使用 RSA/SHA-256 或 RSA/SHA-512,不启用旧的 SHA-1 签名。见 [OpenSSH 说明](https://www.openssh.com/txt/release-8.8)。
4. 管理员登录后调用 `PATCH /api/admin/users/alice`,正文 `{ "enabled": true, "role": "publisher" }`。服务器按公钥绑定的用户分配独立仓库。
5. 此时 SSH 才接受该用户的密钥;`reader`、停用用户、未绑密钥的用户均不能访问 Git。管理员也需登记公钥才能发布。旧 `prefixes` 字段仅为兼容保留,不再用于文章隔离。
- 授权与密钥独立:知道服务器地址、有网站账户、上传了公钥都不足以推送。
- 设回 `reader` 或 `enabled:false` 会撤回发布授权,并使现有网站会话失效。
- 禁止管理员停用自己或取消自己的管理角色。
## 本地 Git 工作流
- 先通过可信渠道核对服务器 Ed25519 主机密钥指纹;不要禁用主机密钥检查。
- 主机密钥保存在独立持久卷,重建容器不会更换。
- 所有发布者使用同一个 OS 账户 `git`,身份由对应公钥的强制命令绑定,不由 Git author/email 决定。
```sh
# ~/.ssh/config
Host chublog
HostName blog.example.com
Port 2222
User git
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
# 无论作者是谁,别名都是 content.git;服务端按认证身份映射个人仓库。
# 首次 clone 自动创建服务器裸仓库,作者无须自己创建裸仓库。
git clone chublog:content.git my-articles
cd my-articles
# 仓库已有内容时,每轮编辑前先 git pull --ff-only。
# 创建 index.txt 和正文,格式见下文。
git add index.txt posts/first
git commit -m 'Publish first article'
git push -u origin main
```
## 文章目录与清单
仓库结构示例:
```text
index.txt
posts/
└── first/
├── article.md
└── cover.png
```
`index.txt` 为 UTF-8 三列文本,以空格或 Tab 分隔,无表头,允许空行:
```text
first posts/first article.md
notes posts/my-notes notes.markdown
```
- 三列依次为**文章 ID、相对文件夹、正文文件名**。
- ID 在作者内唯一,支持 1–40 位小写字母、数字及中间短横线,另须避开保留名称。
- 目录必须是 `posts/<目录名>`,目录名遵循 slug 规则,最多 80 字节;文件名只允许 ASCII 字母、数字、短横线、下划线与点,不能以点开头或含 `..`。
- 同一目录不能重复登记;正文可以任意命名,扩展名为 `.md`、`.markdown` 或 `.txt`。
- 索引所指目录和正文必须存在,路径穿越和子目录正文被拒绝。
- 索引上限 64 KiB、1000 项。
## 正文与图片
- 正文沿用 YAML Frontmatter(参考[正文样例](../content/posts/welcome/index.md)),包含 `title/date/updated/status/tags/description`;`slug` 可省略,如填写则必须等于索引 ID。
- `.txt` 正文也需要 Frontmatter,API 通过 `format: text` 表明正文应作为纯文本展示;其他正文为 `format: markdown`。
- 图片置于同一目录,通过 `![说明](cover.png)` 引用。
## 新增、修改与撤回
| 作者操作 | 系统处理 |
| --- | --- |
| 新建正文并在索引新增一行 | 校验后导入文章,在 ChuSQL 创建作者、ID、正文路径、状态等元信息 |
| 修改正文或索引中的目录/文件名,保留 ID | 更新同一文章;站内 ID 不随目录移动而变化 |
| 删除索引中的一行 | 移除当前阅读内容、图片入口及对应数据库元信息,无需先删除本地正文 |
| 把索引清空 | 移除该作者所有当前文章,不影响其他作者 |
- 未被索引选中的正文及目录视为无效内容,不导入阅读快照或数据库;图片只有被选中正文引用才作为公开资源导入;仍计算仓库容量并执行安全校验。
- 删除正文但保留其索引项属于错误,整次推送会被拒绝。
- `status: draft` 可保留登记但不公开;物理删除文件和撤回文章均不会擦除 Git 历史。
## 提交与并发限制
- MVP 每次只接受**一个直接继承个人远端 main 的提交**,首次仅允许一个根提交。
- 禁止 merge、force push、删除 main、标签及其他分支;多次本地提交应先整理为一次提交,再变基到最新远端。
- 同一作者在多台电脑操作仍需处理并发冲突,不同作者互不争用同一个仓库锁。
## 作者隔离与文章地址
- 站内文章 ID 为 `作者名~文章ID`,例如 Alice 的 `first` 对应 `/api/posts/alice~first`;不同作者可以使用相同的本地 ID。
- 服务端分别保存每位作者的 Git 对象与历史,SSH 不能指定别人的仓库路径;作者无法通过 clone 或按提交哈希 fetch 读取其他作者草稿。
- 作者自己已克隆的历史仍可在本地保留,不能靠撤回收回副本。
## 发布状态
- API 自动同步,一般约 2 秒,数据库重建、失败重试或较多作者可能延迟。
- `GET /api/admin/status` 的 `commit` 是作者提交映射的集合摘要,不是任一作者 Git 提交;`indexed_commit` 表示数据库已同步的集合版本。
- Git 接受表示接收校验完成,不保证数据库已同步。
- 当前不做站长内容审核,审核计划见 [TODO](../TODO.md)。
+50
View File
@@ -0,0 +1,50 @@
# 内容限制与隔离边界
[返回项目首页](../README.md)
## 文件白名单
- 仅允许根目录 `index.txt` 和 `posts/<目录>/<文件>` 内的 `.txt/.md/.markdown/.png/.jpg/.jpeg/.webp` 普通文件;扩展名使用小写。
- 拒绝证书扩展名、隐藏文件、`.gitignore`、脚本、配置、符号链接、可执行位、子模块和其他类型。
- 文本中的 PEM 证书/密钥标记也会拒绝。
- Git 接收层能直接拦截,因此非白名单内容使整次推送失败,不静默改变作者提交。
## 容量配额
- 每个文本文件上限 256 KiB;图片单张 2 MiB、每目录最多 12 张、尺寸 4096×4096;每个目录合计 6 MiB。
- 每位作者最多 1000 个目录、14001 个文件、原始文件合计 64 MiB;全站导入快照另限 64 MiB、1000 篇,超出时等待管理员处理。
## Git 接收限额
- Git 输入 pack 上限 8 MiB;全部作者接收仓库合计达到 256 MiB 后拒绝新 SSH 会话,需要管理员离线维护。
- 此阈值是应用检查,生产还应为 Docker 数据盘配置真实文件系统配额。
## 正文解析
- 使用成熟 Markdown 解析器,拒绝原始 HTML、危险链接协议、外链图片、控制符和双向隐藏字符;代码块是惰性文本,绝不执行 MDX、JS、仓库脚本、构建任务或上传内容。
## SSH 命令与密钥
- 支持 Ed25519 和 2048–8192 位 RSA 公钥,拒绝弱密钥、负数/畸形 RSA 参数及额外二进制数据;拒绝把 `authorized_keys` 选项当作公钥上传。
- SSH 只接受精确的 `git-receive-pack 'content.git'` / `git-upload-pack 'content.git'`,没有 shell、SCP/SFTP、TTY、转发、隧道、用户环境变量。
## 进程与存储隔离
- 接收仓库由 UID 10002 管理;API 为独立 UID 10001,正式仓库/用户密码文件不挂载给 SSH 容器;SSH 只读授权文件和只读固定 hook。
- 接收内容再次验证后才导入 API 自己的按作者隔离的 Git 缓存与阅读快照。
## HTTP 与账户限额
- HTTP 请求体 8 KiB;注册/登录合计全局 10 次/分钟,修改操作全局 60 次/分钟;密码哈希并发最多 2。
- 1000 用户、1000 个有效会话上限,一小时固定会话期限。
- 全局限制偏保守,后续可细化 IP/账户限制以改善公平性。
## 统计与隐私
- 统计不收集 IP/访客身份:公开文章数、标签数、Markdown 字节数、启动以来文章 API 请求数。
- 请求数不是独立访客数,重启后清零。
## 前端接入
- 服务器只返回 JSON Markdown 与图片;后续前端必须把元数据当文本输出,并继续禁用原始 HTML、MDX和危险链接,不能将接口字段直接作为 HTML 执行。
- 隔离不能替代 Git/OpenSSH/解码库的持续安全更新。