docs: refresh public docs for current codebase, add SECURITY.md
- README: real counts (44 commands, 61 integration asserts, demo=25 records + 4 semesters + 7 attachments), add resume/assessment module features, env vars incl. LSKY, doc navigation table, done-roadmap items checked - docs/api.md: /auth/logout, admin users/demo-rebuild/sync-lsky routes, attachment thumb/download channels, profile_upsert top-key merge semantics, full footprint_list query reference, 44-command table - sync-server/README: replace stale ADMIN_TOKEN/X-Admin-Token admin section (actual is_admin Bearer model), add go-live checklist, backup note for WAL - CHANGELOG: add Unreleased section (resume/assessment modules, security fixes, perf tuning) - CONTRIBUTING: 61 asserts, security-sensitive-change guidance, perf baseline table, security issue routing - docs/SECURITY.md: new - report channel, threat model table, hardening summary, known tradeoffs - LICENSE: ensure MIT file matches README badge
This commit is contained in:
@@ -28,3 +28,24 @@
|
||||
### Security
|
||||
- scrypt 口令散列 + timingSafeEqual 校验
|
||||
- Bearer Token(365 天过期)、按用户数据隔离、邀请码注册门控
|
||||
|
||||
## [未发布]
|
||||
|
||||
### Added
|
||||
- 简历模块:可视化模板配置与 Word 导出(MHTML 格式 .doc,兼容 Word/WPS)
|
||||
- 综合测评模块:五大素质自动计分、按学年统计、证据画廊(单活动全部附件展示)、可编辑评分细则参考(默认值可改可恢复、随档案保存)、学年九格信息表、Word 档案导出(页码页脚/附件矩阵附录/材料图题注)
|
||||
- `/auth/logout`:服务端吊销当前 Bearer Token;前端登出先吊销再本地清除
|
||||
- 登录限速:同 IP+用户名连续失败 5 次冷却 10 分钟
|
||||
- `profile_upsert` 改为顶层 key 合并(info/resume/zongce 三子文档互不覆盖)
|
||||
- 用户库 WAL + synchronous=NORMAL + 8MB 页缓存(3000 条写入实测 7.7×)
|
||||
- 设备 last_seen_at 回写节流(>60s 才写)
|
||||
|
||||
### Security
|
||||
- 修复:客户端走私 `seed` 标志可绕过邀请码/名额并因自动升管理员规则直接获得管理员权限(`/auth/register` 现强制剥离)
|
||||
- 修复:附件 docx/xlsx 预览 HTML 未消毒(现经白名单式净化,防脚本注入偷取本地会话)
|
||||
|
||||
### Fixed
|
||||
- 综测分学年学期解析(`2023-2024-1` 末段误解析导致该学年学业分缺失)
|
||||
- 综测学业绩点与成绩页学期卡口径不一致(现同源优先)
|
||||
- 档案识别学年错误(按入学时间推算,缺省回退最早学期)
|
||||
- 证据画廊「计分/未计分」徽章在聚合计分项下恒错
|
||||
|
||||
+21
-7
@@ -11,27 +11,41 @@
|
||||
## 开发流程
|
||||
|
||||
1. Fork / 建分支:`git checkout -b feat/your-feature`
|
||||
2. 提交信息用约定式前缀:`feat: / fix: / docs: / chore: / ci: / refactor:`
|
||||
2. 提交信息用约定式前缀:`feat: / fix: / docs: / chore: / ci: / refactor: / security: / perf:`
|
||||
3. 本地跑绿再推:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks # 每个克隆只需一次
|
||||
node scripts/check.mjs
|
||||
node scripts/test-integration.mjs
|
||||
node scripts/test-integration.mjs # 61 条集成断言
|
||||
```
|
||||
|
||||
启用钩子后,push 会自动跑同一套检查;工作区有未提交变更或测试失败时 push 会被拒绝。
|
||||
|
||||
## 约定
|
||||
|
||||
- 服务端保持**零 npm 依赖**(只用 Node 内置模块);确需引入依赖请先开 Issue 讨论
|
||||
- 改动 `app/server/db.mjs`(命令层)时注意:Web / 桌面 / 播种三处共用它,测试必须全绿
|
||||
- 涉及 schema 变更:保证老库能平滑迁移(参考 `preMigrateColumns` 的加列模式),并在
|
||||
`scripts/test-integration.mjs` 补断言
|
||||
- **服务端保持零 npm 依赖**(只用 Node 内置模块);确需引入依赖请先开 Issue 讨论
|
||||
- 改动 `app/server/db.mjs`(命令层)时注意:Web / 桌面 / 播种三处共用它,测试必须全绿;新增命令顺手在 `docs/api.md` 补一行
|
||||
- 涉及 schema 变更:保证老库能平滑迁移(参考 `schema.sql` IF NOT EXISTS / `preMigrateColumns` 的加列模式),并在 `scripts/test-integration.mjs` 补断言
|
||||
- 涉及 Service Worker:接口类请求(/api /auth /admin /sync)永远不得缓存
|
||||
- UI 改动请同时适配亮/暗两套主题(令牌见 `app/src/styles/theme.css`)
|
||||
- UI 改动请同时适配亮/暗两套主题(令牌见 `app/src/styles/theme.css`),并兼顾移动端(390px 宽)与浏览器打印样式
|
||||
- **安全敏感路径**(认证/上传/管理/导出)请说明改动思路;过滤、白名单优先于黑名单;欢迎补 PoC 级测试(参考 `docs/SECURITY.md` 的字段表)
|
||||
|
||||
## Issue
|
||||
|
||||
- Bug 报告请附:复现步骤、期望/实际行为、服务端日志(`docker logs footprints-sync`)
|
||||
- 功能建议请说明使用场景,最好附上你期望的记录/检索形态
|
||||
- 安全问题请走 [`docs/SECURITY.md`](docs/SECURITY.md) 的渠道,勿在公开 Issue 贴 PoC 细节
|
||||
|
||||
## 性能基线(改数据层请起压测)
|
||||
|
||||
命令层基准(3000 条记录、PC 单机):
|
||||
|
||||
| 命令 | 耗时 |
|
||||
|---|---|
|
||||
| footprint_list(2000 条一页)| ≤23ms |
|
||||
| timeline | ≤22ms |
|
||||
| 单条 footprint_upsert | ≈0.3ms |
|
||||
|
||||
回归手动基准:`node .tmp-perf/bench.mjs` 模式(见 git 历史样例);
|
||||
引入 >2× 的回退需要说明必要性(一般值得换改造思路)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 linyux (footpr.linyux.cn)
|
||||
Copyright (c) 2026 Footprints Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -12,20 +12,22 @@
|
||||
|
||||
- **留痕本体** —— 自定义记录类型 + 动态表单模板;竞赛、志愿、学生工作、荣誉、证书、科研、实习皆可配;正面/负面、进行中、草稿全留痕
|
||||
- **秒级检索** —— FTS5 trigram 中文全文搜索 + 多条件筛选 + 保存视图(同一查询一键复用)
|
||||
- **时间线** —— 按学年/月分组的完整回顾
|
||||
- **学业成绩** —— 学期 GPA/排名、课程成绩、可自定义绩点梯度(grade band)与趋势图
|
||||
- **时间线** —— 按学年/月分组的完整回顾,玻璃质感粘性年份头
|
||||
- **学业成绩** —— 学期 GPA/排名、课程成绩、可自定义绩点梯度(grade band)与趋势图;综测学业分直接取学期卡同源口径
|
||||
- **成长量化** —— 自定义维度与指标(如志愿时长),统计总览与年度对比
|
||||
- **附件佐证** —— 图片/文档上传,SHA-256 内容寻址去重
|
||||
- **简历** —— 可视化编辑模板(基本信息/学业/经历区块增删排序)+ 一键导出规范排版的 Word 文档
|
||||
- **综合测评** —— 五大素质自动计分(活动/竞赛/学生工作/奖惩,逐项证据画廊)、中文 FTS 三角分词检索佐证、可编辑评分细则与手动修正、学年九格信息表、导出带页码页脚与附件矩阵附录的 Word 档案
|
||||
- **附件佐证** —— 图片/文档/PDF 上传,SHA-256 内容寻址去重,客户端缩略图 + 灯箱预览,docx/xlsx 在线预览(已 HTML 加固)
|
||||
- **多用户云优先** —— 所有数据存服务器,按用户独立 SQLite 档案库;浏览器 / PWA / Windows 桌面三端同源同步
|
||||
- **管理后台** —— 邀请码全生命周期管理(自定义码、备注、启用/停用、删除、批量生成)、用户列表、注册配额
|
||||
- **演示模式** —— 内置演示账号 `demo / demo1234`,自动播种 16 条足迹 + 4 学期成绩,开箱即看
|
||||
- **管理后台** —— 邀请码全生命周期管理(自定义码、备注、启用/停用、删除、批量生成)、用户列表、注册配额、账号删除级联清理(图床/档案库/附件/会话)
|
||||
- **演示模式** —— 内置演示账号 `demo / demo1234`,自动播种 25 条足迹 + 4 学期成绩 + 7 份佐证附件,开箱即看
|
||||
- **PWA + 桌面** —— 可安装到桌面/手机;Windows Tauri 安装包;亮/暗双主题
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
浏览器 PWA ─┐
|
||||
Windows 桌面 ─┤ HTTPS + Bearer Token(365 天)
|
||||
Windows 桌面 ─┤ HTTPS + Bearer Token(365 天,可服务端吊销)
|
||||
移动端 PWA ─┘
|
||||
│
|
||||
▼
|
||||
@@ -33,17 +35,19 @@
|
||||
│ sync-server(零依赖 Node ≥ 22.5)│
|
||||
│ node:http + node:sqlite │
|
||||
│ scrypt 认证 · 邀请码 · LWW 同步 │
|
||||
│ 登录限速 · 会话吊销 · 附件加固 │
|
||||
└───────────────┬────────────────┘
|
||||
▼
|
||||
data/users/<uid>.db 每用户独立 SQLite 档案库
|
||||
data/blobs/<uid>/ 内容寻址附件存储
|
||||
data/users/<uid>.db 每用户独立 SQLite 档案库(WAL + 索引齐全)
|
||||
data/attachments/ 按用户隔离的内容寻址附件
|
||||
```
|
||||
|
||||
设计取向:
|
||||
|
||||
- **服务器零 npm 依赖**:仅用 Node 内置模块(`node:http` / `node:sqlite` / `node:crypto`),攻击面与供应链风险极小
|
||||
- **命令层同源**:Web、桌面、数据播种共用同一份 `app/server/db.mjs` 命令层,同一业务逻辑多处不漂移
|
||||
- **命令层同源**:Web、桌面、数据播种共用同一份 `app/server/db.mjs` 命令层(44 个命令),同一业务逻辑多处不漂移
|
||||
- **同步元数据内建**:所有表带 uuid/hlc/origin 列 + oplog,为多端增量同步而生
|
||||
- **性能基线**(3000 条记录实测):列表/时间线 ≤23ms、单条写 ≈0.3ms(WAL + synchronous=NORMAL + 8MB 页缓存)
|
||||
|
||||
## 快速开始(Docker 部署)
|
||||
|
||||
@@ -69,12 +73,13 @@ footprints.example.com {
|
||||
```
|
||||
|
||||
> compose 默认加入名为 `mynet` 的外部网络;没有就先 `docker network create mynet`,或把 compose 里的 networks 段改成你自己的。
|
||||
> 详细部署与拓扑说明见 [`sync-server/README.md`](sync-server/README.md)。
|
||||
|
||||
首次使用:
|
||||
|
||||
1. 打开站点 → 注册 —— **第一个注册的账号自动成为管理员**
|
||||
1. 打开站点 → 注册 —— **第一个注册的账号自动成为管理员**(内置 demo 账号不占管理员席位)
|
||||
2. 管理员登录 → 侧边栏「管理」→ 新建/批量生成邀请码,发给需要注册的用户
|
||||
3. 人齐之后建议收紧注册:compose 里 `REGISTER_ENABLED=false`(或改掉 `INVITE_CODE` 默认值)再 `up -d`
|
||||
3. 人齐之后建议收紧注册:compose 里 `REGISTER_ENABLED=false`,并把 `INVITE_CODE` 从示例默认值 `please-change-me` 换成强随机值或留空(改用管理页动态码)
|
||||
4. 想先看看界面?用演示账号 **demo / demo1234** 登录,数据管够
|
||||
|
||||
## 本地开发
|
||||
@@ -99,29 +104,32 @@ pnpm -C app install && pnpm -C app tauri build
|
||||
|
||||
```bash
|
||||
node scripts/check.mjs # 语法 + 类型 + 构建 + 关键标记 tripwire
|
||||
node scripts/test-integration.mjs # 35 条集成断言(播种/登录/邀请码全矩阵/角色规则/幂等)
|
||||
node scripts/test-integration.mjs # 61 条集成断言(播种/登录/邀请码全矩阵/角色规则/幂等/级联删除)
|
||||
node scripts/smoke.mjs <url> <user> <pass> # 部署后冒烟
|
||||
git config core.hooksPath .githooks # 启用 pre-push 钩子:工作区不干净或测试红,push 直接拒绝
|
||||
```
|
||||
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
├─ app/ # 前端 + 桌面端
|
||||
│ ├─ src/ # React 19 + Vite + Tailwind v4 UI(三端共用)
|
||||
│ ├─ public/ # PWA:sw.js v3(接口绝不缓存)/ manifest / icons
|
||||
│ ├─ server/ # ★ 命令层 db.mjs:零依赖数据层(37 个命令)
|
||||
│ │ ├─ pages/ # 时间线/记录/成绩/统计/类型/字典/个人信息/简历/综合测评/管理
|
||||
│ │ ├─ lib/zongce.ts # 综测计分引擎(五大素质聚合 + 学业口径对齐)
|
||||
│ │ └─ styles/theme.css # 亮/暗主题令牌 + MHTML Word 导出样式基座
|
||||
│ ├─ public/ # PWA:sw.js(接口绝不缓存)/ manifest / icons
|
||||
│ ├─ server/ # ★ 命令层 db.mjs:零依赖数据层(44 个命令)
|
||||
│ ├─ schema.sql # 数据库 schema(Rust 与 Node 共用)
|
||||
│ └─ src-tauri/ # Tauri 2 桌面壳
|
||||
├─ sync-server/ # 自建同步服务器
|
||||
│ ├─ src/ # index/auth/db/sync/attachments(零依赖)
|
||||
│ ├─ www/ # 构建产物(部署时以卷挂载,git pull 即更新)
|
||||
│ └─ docker-compose.caddy.yml
|
||||
├─ docs/ # api.md(HTTP API)· DESIGN.md(设计底稿)· examples/
|
||||
├─ docs/ # api.md · DESIGN.md · SECURITY.md · zongce-rules(测评细则示例)
|
||||
├─ scripts/ # check / test-integration / smoke
|
||||
├─ tools/ # pnpm.cjs + 图标生成器
|
||||
├─ .gitea/ # Issue/PR 模板
|
||||
├─ .githooks/ # pre-push 质量门禁(check + 35 条集成断言)
|
||||
├─ .githooks/ # pre-push 质量门禁(check + 61 条集成断言)
|
||||
└─ .editorconfig
|
||||
```
|
||||
|
||||
@@ -131,20 +139,26 @@ git config core.hooksPath .githooks # 启用 pre-push 钩子:工作区不干
|
||||
|---|---|---|
|
||||
| `PORT` | `8788` | 监听端口 |
|
||||
| `DATA_DIR` | `./data` | 数据目录(用户库/附件/同步账本) |
|
||||
| `REGISTER_ENABLED` | `true` | 是否开放注册 |
|
||||
| `INVITE_CODE` | 空 | 静态兜底邀请码(动态码由管理员在管理页生成) |
|
||||
| `REGISTER_ENABLED` | `true` | 是否开放注册(注册完毕后建议关闭) |
|
||||
| `INVITE_CODE` | 空 | 静态兜底邀请码(动态码由管理员在管理页生成;**务必改默认值**) |
|
||||
| `MAX_USERS` | `0` | 用户数上限,0 = 不限 |
|
||||
| `WWW_DIR` | `../www` | 前端静态目录 |
|
||||
| `LSKY_API` / `LSKY_TOKEN` | 空 | 可选:附件自动迁入自建图床(Lsky Pro) |
|
||||
|
||||
## 文档
|
||||
|
||||
- [HTTP API 参考](docs/api.md)
|
||||
- [设计方案(v0.5 底稿)](docs/DESIGN.md) —— 项目的原始设计思想;现产品已演进为云优先多用户架构
|
||||
| 文档 | 内容 |
|
||||
|---|---|
|
||||
| [HTTP API 参考](docs/api.md) | 全部 HTTP 端点与 44 个业务命令 |
|
||||
| [安全模型](docs/SECURITY.md) | 威胁模型、已加固项、漏洞报告渠道 |
|
||||
| [设计方案(v0.5 底稿)](docs/DESIGN.md) | 项目的原始设计思想;现产品已演进为云优先多用户架构 |
|
||||
| [综合测评细则示例](docs/zongce-rules-2026-2027.md) | 「评分细则参考」默认值的来源(纯文本,可自行修改) |
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] 附件缩略图 / 上传前压缩 / 大文件分片
|
||||
- [ ] 评奖评优材料包一键导出
|
||||
- [ ] 大文件分片上传
|
||||
- [x] 附件缩略图 / 上传前压缩(客户端 canvas 生成 480px WebP)
|
||||
- [x] 评奖评优材料包一键导出(简历 / 综测 Word 档案)
|
||||
- [ ] 对象存储(S3)适配
|
||||
- [ ] 端到端加密存储
|
||||
- [ ] 移动端体验优化(手势、离线编辑队列)
|
||||
|
||||
+6
-5
@@ -5,12 +5,13 @@ React 19 前端 + Tauri 2 桌面壳 + 共用 Node 命令层。
|
||||
|
||||
## 结构
|
||||
|
||||
src/ React 前端(时间线 / 记录 / 成绩 / 统计 / 类型 / 字典 / 管理)
|
||||
public/ PWA:sw.js(v3,接口类请求绝不缓存)/ manifest / 图标
|
||||
src/ React 前端(时间线/记录/成绩/统计/类型/字典/个人信息/简历/综合测评/管理)
|
||||
public/ PWA:sw.js(接口类请求绝不缓存)/ manifest / 图标
|
||||
src-tauri/ Tauri 2 桌面壳(Windows NSIS 安装包构建)
|
||||
server/ db.mjs 命令层(37 个命令,Web 与桌面共用)+ seed.mjs(本地演示数据)
|
||||
server/ db.mjs 命令层(44 个命令,Web 与桌面共用)+ seed.mjs(本地演示数据)
|
||||
schema.sql 数据库 schema(Rust 与 Node 共用同一份,v2 含同步元数据)
|
||||
src/lib/zongce.ts 综测计分引擎(细粒度条款聚合 + 学业同源口径)
|
||||
|
||||
测试与约定见 [`../CONTRIBUTING.md`](../CONTRIBUTING.md);架构底稿见 [`../docs/DESIGN.md`](../docs/DESIGN.md);HTTP 契约见 [`../docs/api.md`](../docs/api.md)。
|
||||
|
||||
图标再生成:`python ../tools/gen_icons.py`(桌面)与 `python ../tools/gen_pwa_icons.py`(PWA)。
|
||||
|
||||
文档:[`../docs/DESIGN.md`](../docs/DESIGN.md)(设计底稿)· [`../docs/api.md`](../docs/api.md)(HTTP API)· [`../sync-server/`](../sync-server/)(服务器)。
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# 安全说明
|
||||
|
||||
## 漏洞报告
|
||||
|
||||
发现安全问题请**不要**直接开公开 Issue(可能包含可利用细节)。联系渠道:
|
||||
|
||||
- 仓库主页留下联系方式的维护者(站内私信 / Issues 留言板)
|
||||
- 紧急情况可直接在描述安全隐患的前提下开 Issue 标注 `security`,我们会尽快下线和修复
|
||||
|
||||
处理流程:收到报告 → 48 小时内确认 → 评估影响并给出缓解策略 → 修复发版 → 在 `CHANGELOG.md` 的 `Security` 段致谢公示。
|
||||
|
||||
## 安全模型(威胁模型摘要)
|
||||
|
||||
数据全走 HTTPS + Bearer Token,服务器为多用户托管形态,按用户强制隔离——信任边界假设
|
||||
「攻击者 = 其他注册用户 / 网络中间人 / 触碰 GET 接口的匿名流量」。
|
||||
|
||||
| 面 | 现状 |
|
||||
|---|---|
|
||||
| 认证算法 | scrypt(随机 16B 盐)+ `timingSafeEqual`,官方 crypto 原语,无自造算法 |
|
||||
| Token | 服务端 32 字节随机 hex 存储 + 365 天过期;登录后同设备复用 device_id;支持 `/auth/logout` 服务端吊销 |
|
||||
| 类似爆破的防护 | 登录:同 IP+用户名连续失败 5 次 → 冷却 10 分钟(进程内计数) |
|
||||
| 按用户数据隔离 | `commandsFor(user_id)` 每用户一份独立 `users/<uid>.db`;跨库读取天然命中"不存在",实测经 /api 与 /sync 均无法跨读 |
|
||||
| 管理 API | 统一双重校验:登录态 + is_admin;自删与"最后管理员"禁删 |
|
||||
| SQL 注入 | 全部参数化;`ORDER BY` 走白名单(`SORTS` 表);FTS MATCH 参数传入、字符串仅做双引号包裹 |
|
||||
| 路径注入/XSS | 上传文件名剥离 `\\/:*?"<>|` + UUID 前缀;附件文件名以 **SHA-256 命名**(无文件系统注入位);前端 `dangerouslySetInnerHTML` 有且仅两处(一处全转义、一处白名单净化 docx/xlsx 预览) |
|
||||
| 注册准入 | `REGISTER_ENABLED`、`INVITE_CODE`(或管理页动态码)、`MAX_USERS` 三道门控,服务端强制(客户端传的 `seed` 播种标志一律剥离,防止绕过邀请码/配额/自动升级管理员规则) |
|
||||
| 附件上传 | SHA-256 声明校验 + 100MB 上限;下载统一 `Cache-Control: private, immutable`(跨账号缓存隔离) |
|
||||
| 客户端依赖 | 服务器**零 npm 依赖**(仅 Node 内置模块),规避供应链攻击面 |
|
||||
| CORS | `Access-Control-Allow-Origin: *`。凭据仅 localStorage Bearer、不用 Cookie,不放大 CSRF 面;如有同源严格化诉求欢迎提案 |
|
||||
| 审计日志 | 图床清理、用户删除、demo 重建等敏感操作写标准日志(容器 stdout) |
|
||||
|
||||
## 已知设计取舍
|
||||
|
||||
- Token 存放浏览器 `localStorage`(XSS 可及):与多端 token 缓存目录一致;服务端可能追加 CSP 响应头、可加入 HttpOnly Cookie 会话机制(改动契约,暂未引入)
|
||||
- 限速是进程内存态,重启清零;RLS 想要更强的反爆破可以套容器内的 fail2ban / Cloudflare 规则
|
||||
- 备份文件内含用户档案明文,请按敏感数据管理
|
||||
|
||||
## 上线运维要点(契约要求)
|
||||
|
||||
- 首个真实账号注册后关闭 `REGISTER_ENABLED`,改掉 `INVITE_CODE` 示例默认值
|
||||
- `./data/` 目录备份(含 `-wal`/`-shm`)
|
||||
- 服务端公网面只暴露 443(Caddy TLS 到 sync-server)
|
||||
+48
-17
@@ -1,7 +1,7 @@
|
||||
# HTTP API 参考
|
||||
|
||||
Base URL:部署域名(如 `https://footpr.example.com`)。除 `/health` 与 `/auth/register|login` 外,
|
||||
所有接口都需要 `Authorization: Bearer <token>`。
|
||||
所有接口都鉴权。登录限速:同一 IP + 用户名连续失败 5 次会收到 429,冷却 10 分钟。
|
||||
|
||||
统一响应格式:
|
||||
|
||||
@@ -20,29 +20,32 @@ Base URL:部署域名(如 `https://footpr.example.com`)。除 `/health`
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| POST | `/auth/register` | 注册(`REGISTER_ENABLED`/`INVITE_CODE`/`MAX_USERS` 门控;首位用户自动成为管理员) |
|
||||
| POST | `/auth/register` | 注册(`REGISTER_ENABLED`/邀请码/`MAX_USERS` 门控;首个非 demo 账号自动成为管理员)。请求体里的 `seed` 标志一律被服务端剥离,仅供内部播种 |
|
||||
| POST | `/auth/login` | 登录,返回 Bearer Token(365 天) |
|
||||
| POST | `/auth/logout` | 吊销当前 Bearer Token(服务端删除 token 行) |
|
||||
| GET | `/auth/me` | 当前用户信息(`user_id` / `username` / `is_admin` / `device_id`) |
|
||||
|
||||
```bash
|
||||
curl -s https://footpr.example.com/auth/login \
|
||||
TOKEN=$(curl -s https://footpr.example.com/auth/login \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"username":"demo","password":"demo1234","device_name":"cli"}'
|
||||
# => {"ok":true,"data":{"token":"...","user_id":"...","device_id":"...","expires_at":"..."}}
|
||||
-d '{"username":"demo","password":"demo1234","device_name":"cli"}' | jq -r .data.token)
|
||||
```
|
||||
|
||||
## 管理(仅 is_admin 会话)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/admin/users` | 用户列表(设备数/记录数/附件用量) |
|
||||
| GET | `/admin/invites` | 邀请码列表(含 remark、创建者、使用者) |
|
||||
| GET | `/admin/users` | 用户列表(设备数/记录数/附件用量),元数据不含档案内容 |
|
||||
| POST | `/admin/users/delete` | 删除用户 `{"user_id":"..."}`,级联清理图床/档案库/附件/会话;不能删自己、不能删最后一位管理员 |
|
||||
| GET | `/admin/invites` | 邀请码列表(remark、创建者/使用者、valid、holder_gone) |
|
||||
| POST | `/admin/invites/generate` | 批量生成随机码 `{"count": 5}`(1-50) |
|
||||
| POST | `/admin/invites/create` | 新建自定义码 `{"code":"XIONG-2025","remark":"备注"}`(4-32 位字母数字-\_) |
|
||||
| POST | `/admin/invites/create` | 新建自定义码 `{"code":"XIONG-2025","remark":"备注"}`(4-32 位字母数字-_) |
|
||||
| POST | `/admin/invites/update` | 改备注 / 启停 `{"id":1,"remark":"...","valid":false}`(已使用的码不可再启用) |
|
||||
| POST | `/admin/invites/delete` | 删除 `{"id":1}` |
|
||||
| POST | `/admin/invites/revoke` | 作废(等价 update valid=false,兼容保留) |
|
||||
| POST | `/admin/demo-seed` | 给 demo 账号播种演示数据(幂等;档案库非空时返回 seeded:false) |
|
||||
| POST | `/admin/demo-rebuild` | 级联删除现有 demo 并重建、重播种演示数据 |
|
||||
| POST | `/admin/demo-seed` | 给 demo 播种演示数据(幂等;档案库非空时返回 seeded:false) |
|
||||
| POST | `/admin/attachments/sync-lsky` | 把当前用户全部未上图床的附件批量推送到 Lsky(需配置 `LSKY_API`/`LSKY_TOKEN`) |
|
||||
|
||||
## 同步
|
||||
|
||||
@@ -51,12 +54,15 @@ curl -s https://footpr.example.com/auth/login \
|
||||
| GET | `/sync/status` | 当前用户同步账本统计 |
|
||||
| POST | `/sync/push` | 推送 wire 变更(LWW 上/删除,立即生效) |
|
||||
| GET | `/sync/pull?cursor=...&limit=...` | 按 seq 游标增量拉取 |
|
||||
| POST | `/sync/attachment` | 上传附件 `{"sha256","size","mime","file_name","data_b64"}`(内容寻址去重) |
|
||||
| GET | `/sync/attachment/<sha256>` | 下载附件 |
|
||||
| POST | `/sync/attachment` | 上传附件 `{"sha256","size","mime","file_name","data_b64"}`(内容寻址去重,哈希校验) |
|
||||
| GET | `/sync/attachment/<sha256>` | 下载附件(`Cache-Control: private, immutable`) |
|
||||
|
||||
附件另有两个业务命令通道:`POST /api/attachment_read`(base64 全文,含缩略图通道 `attachment_thumb`)
|
||||
与 `POST /api/attachment_download`(直接落盘,`Content-Disposition` 带原始文件名)。
|
||||
|
||||
## 业务命令层 `POST /api/<command>`
|
||||
|
||||
与桌面端共用同一命令层(`app/server/db.mjs`),按用户隔离。37 个命令:
|
||||
与桌面端共用同一命令层(`app/server/db.mjs`),按用户隔离。44 个命令:
|
||||
|
||||
| 分组 | 命令 |
|
||||
|---|---|
|
||||
@@ -65,11 +71,39 @@ curl -s https://footpr.example.com/auth/login \
|
||||
| 字典/维度 | `taxonomy_list` · `taxonomy_upsert` · `taxonomy_delete` · `dimension_list` · `dimension_upsert` · `dimension_delete` |
|
||||
| 足迹 | `footprint_list` · `footprint_get` · `footprint_upsert` · `footprint_delete` · `timeline` |
|
||||
| 统计 | `stats_overview` · `metric_list` · `metric_upsert` · `metric_delete` |
|
||||
| 成绩 | `grades_overview` · `semester_upsert` · `semester_delete` · `course_upsert` · `course_delete` · `band_upsert` · `band_delete` |
|
||||
| 附件 | `attachment_add` · `attachment_list` · `attachment_delete` · `attachment_read` |
|
||||
| 成绩 | `grades_overview` · `semester_upsert` · `semester_delete` · `course_upsert` · `course_delete` · `band_upsert` · `band_delete` · `gpa_settings_get` · `gpa_settings_upsert` |
|
||||
| 附件 | `attachment_add` · `attachment_list` · `attachment_delete` · `attachment_read` · `attachment_thumb` · `attachment_set_remote` · `attachment_set_thumb` |
|
||||
| 档案 | `profile_get` · `profile_upsert` |
|
||||
| 保存视图 | `view_list` · `view_upsert` · `view_delete` |
|
||||
| 播种/同步内核 | `demo_seed` · `sync_apply_wire` · `sync_pull_wire` |
|
||||
|
||||
### `profile_upsert` 语义
|
||||
|
||||
顶层 key 合并:`info` / `resume` / `zongce` 是互相独立的子文档,只更所提交的 key,
|
||||
其余原样保留(避免三张页面互相覆盖):
|
||||
|
||||
```json
|
||||
{ "info": { "name": "...", "phone": "...", "eduStart": "2023-09" } }
|
||||
```
|
||||
|
||||
### `footprint_list` 的 `query`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `q` | 全文检索(≥3 字符走 FTS5 trigram 子串匹配,短词回退 LIKE) |
|
||||
| `year` | 起始年份过滤 |
|
||||
| `types` / `levels` / `roles` / `statuses` | 数组多选过滤 |
|
||||
| `dims` | 维度多选 |
|
||||
| `polarity` | `positive` / `negative` |
|
||||
| `has_attachment` | 仅看有附件记录 |
|
||||
| `tag` | 标签匹配 |
|
||||
| `sort` | `date_desc`(默认)/ `date_asc` / `updated_desc` / `weight_desc` |
|
||||
| `page` /`page_size` | 分页(`page_size` 上限 2000) |
|
||||
|
||||
### 分页约定
|
||||
|
||||
`page_size` 上限 2000;传非 `{query:{...}}` 形状的载荷会按默认 20 条处理(客户端请按此约定传参)。
|
||||
|
||||
示例(检索记录):
|
||||
|
||||
```bash
|
||||
@@ -77,6 +111,3 @@ curl -s https://footpr.example.com/api/footprint_list \
|
||||
-H 'content-type: application/json' -H "authorization: Bearer $TOKEN" \
|
||||
-d '{"query":{"page":1,"page_size":20,"year":2025}}'
|
||||
```
|
||||
|
||||
`footprint_list` 的 `query` 支持:`year`、`type_id`、`level`、`status`、`q`(全文检索)、
|
||||
`dimension_id`、`sort`(date_desc/date_asc/updated_desc/weight_desc)、分页 `page`/`page_size`。
|
||||
|
||||
+31
-20
@@ -1,7 +1,7 @@
|
||||
# Footprints 自建同步服务
|
||||
|
||||
个人留痕系统的多端同步服务端。零 npm 依赖(Node 内置 node:sqlite),SQLite 单文件存储,
|
||||
专为单用户/小规模多设备设计。协议:行级 LWW(HLC 仲裁)+ seq 游标增量拉取。
|
||||
个人留痕系统的多端同步服务端。零 npm 依赖(Node 内置 node:sqlite),SQLite 单文件存储。
|
||||
协议:行级 LWW(HLC 仲裁)+ seq 游标增量拉取;每个用户一个独立 SQLite 档案库。
|
||||
|
||||
## 部署(Docker)
|
||||
|
||||
@@ -24,41 +24,51 @@
|
||||
docker compose up -d --build
|
||||
# 默认只绑定 127.0.0.1:8788,交给宿主上的反代;需要直连时改为 "8788:8788"
|
||||
|
||||
数据全部落在 `./data/`(SQLite 库 + 附件 Blob),备份 = 复制该目录。
|
||||
数据全部落在 `./data/`(SQLite 库 + 附件),备份 = 复制该目录(WAL 模式建议连同 `-wal`/`-shm` 一起拷,或先 `wal_checkpoint(TRUNCATE)`)。
|
||||
|
||||
- 首次部署 `REGISTER_ENABLED=true`,注册自己的账号后改为 `false` 再 `docker compose up -d` 关闭注册
|
||||
- 验证:curl https://footpr.linyux.cn/health 应返回 {"ok":true,...}
|
||||
### 上线检查清单
|
||||
|
||||
- [ ] `INVITE_CODE` 改掉示例默认值 `please-change-me`(或留空改用管理页动态码)
|
||||
- [ ] 第一个真实账号注册完成后 `REGISTER_ENABLED=false` 再 `docker compose up -d`
|
||||
- [ ] `MAX_USERS` 按需限额(0 = 不限)
|
||||
- [ ] `curl https://你的域名/health` 返回 `{"ok":true,...}`
|
||||
- [ ] `./data/` 已纳入备份计划
|
||||
|
||||
## 多用户
|
||||
|
||||
不止一人使用时,服务端天然按账号隔离数据(变更/附件/设备均挂 user_id),无需分库:
|
||||
服务端天然按账号隔离数据(每用户独立 `users/<uid>.db` 档案库,附件按用户子目录隔离),无需分库:
|
||||
|
||||
- 邀请码:`INVITE_CODE` 设置后,注册请求必须携带 `invite_code` 字段——把邀请码发给你信任的人即可
|
||||
- 邀请码:`INVITE_CODE` 设置后,注册请求必须携带 `invite_code` 字段——把邀请码发给你信任的人即可;动态码由管理员在「管理」页生成(支持备注/启停/删除/批量)
|
||||
- 名额:`MAX_USERS` 限制用户总数(0 = 不限),防滥用
|
||||
- 管理:设置 `ADMIN_TOKEN` 后,`GET /admin/users`(请求头 `X-Admin-Token`)可查看全部用户、设备数、记录数与附件占用
|
||||
- 管理:首位真实注册账号自动成为管理员(内置 demo 账号不占席位且永不为管理员);管理员登录后「管理」页可查看全部用户(设备数/记录数/附件占用)、删除用户(级联清理其图床文件/档案库/附件/会话)、重建演示账号
|
||||
- 客户端:各用户安装桌面应用或 PWA,登录自己的账号即得到独立档案;同一台电脑可切换多个账号(本地档案按账号隔离)
|
||||
- 数据安全:账号间完全隔离;管理员也只能看到元数据(用户名/设备数/存储占用),看不到档案内容
|
||||
|
||||
## 安全要点
|
||||
## 安全
|
||||
|
||||
- 必须经 HTTPS 访问(Caddy 自动证书);裸 HTTP 暴露公网不可接受
|
||||
- 数据(SQLite + 附件 Blob)全在 `./data/`,定期备份该目录即可
|
||||
- 服务端不解释业务字段;表间引用由客户端以 uuid 表达
|
||||
- 口令 scrypt 散列 + `timingSafeEqual` 校验;Bearer Token 365 天有效期,支持服务端吊销(`POST /auth/logout`)
|
||||
- 登录限速:同 IP + 同用户名连续失败 5 次冷却 10 分钟
|
||||
- 用户档案库仅用参数化 SQL;附件以 SHA-256 内容寻址命名(无路径可穿越)、哈希声明校验、100MB 单文件上限
|
||||
- 详见 [`../docs/SECURITY.md`](../docs/SECURITY.md)
|
||||
|
||||
## API
|
||||
|
||||
完整参考见 [`../docs/api.md`](../docs/api.md)。同步面速览:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | /health | 健康检查(Docker healthcheck 使用) |
|
||||
| POST | /auth/register | 注册 {username, password, device_name},返回 {token, device_id} |
|
||||
| POST | /auth/register | 注册 {username, password, invite_code, device_name},返回 {token, device_id} |
|
||||
| POST | /auth/login | 登录,同上;同名设备复用 device_id |
|
||||
| POST | /auth/logout | 吊销当前 Bearer Token |
|
||||
| GET | /auth/me | 当前用户信息 |
|
||||
| POST | /sync/push | 推送变更 {changes:[{table,row_uuid,op,hlc,data}]},返回 {accepted, stale, invalid} |
|
||||
| GET | /sync/pull?cursor=N | 增量拉取,返回 {changes, cursor, has_more} |
|
||||
| POST | /sync/attachment | 上传附件 {sha256,size,mime,file_name,data_b64},返回 {dedup} |
|
||||
| GET | /sync/attachment/:sha256 | 下载附件(内容寻址,跨设备去重) |
|
||||
| GET | /sync/status | 账号统计 |
|
||||
| POST | /api/footprint_list 等 | 业务命令层 `POST /api/<command>`(44 个命令,按用户隔离) |
|
||||
| POST | /api/attachment_download | 附件落盘下载(Content-Disposition 带原始文件名) |
|
||||
|
||||
鉴权:`Authorization: Bearer <token>`(/sync/* 全部需要)。
|
||||
鉴权:`Authorization: Bearer <token>`(除 /health /auth/register /auth/login 外全部需要)。
|
||||
|
||||
### 同步语义
|
||||
|
||||
@@ -69,14 +79,15 @@
|
||||
|
||||
## 目录
|
||||
|
||||
src/index.mjs HTTP 路由
|
||||
src/db.mjs SQLite schema 与连接
|
||||
src/auth.mjs 注册/登录/token(scrypt)
|
||||
src/index.mjs HTTP 路由(静态/认证/管理/同步/命令层)
|
||||
src/db.mjs Service 主库 schema(users/devices/tokens/invites)
|
||||
src/auth.mjs 注册/登录/token/登出(scrypt + 限速)
|
||||
src/sync.mjs push(LWW)/ pull(seq 游标)
|
||||
src/app-commands.mjs 桥接共用命令层(app/server/db.mjs)
|
||||
src/attachments.mjs 附件内容寻址存储
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `docker-compose.caddy.yml`:与既有 Caddy 容器网络配合(推荐)
|
||||
- `docker-compose.yml`:独立部署(本机回环端口)
|
||||
- `Caddyfile.snippet`:Caddyfile 增量片段
|
||||
- `Caddyfile.snippet`:Caddyfile 增量片段
|
||||
|
||||
Reference in New Issue
Block a user