17 Commits

Author SHA1 Message Date
xfy
65a7b1226f style: 全项目格式规范化(cargo fmt)+ Docker daemon 断连容错
- 全项目 .rs 统一 cargo fmt 排版(换行/缩进/参数分行/导入排序)
- infra/docker: DOCKER_CLIENT 连接失败不 panic,降级返回 None
  (博客不依赖 Docker,panic=abort 下会导致整个进程崩溃)
- infra/mod: 去除多余空行
- database/mod: 模块声明按字母序重新排序
2026-07-15 17:22:06 +08:00
xfy
2310ed9abe fix(system): 备份恢复实际不写入数据 + 假成功
三层叠加 bug 导致「恢复完成却零数据变更」:

1. 备份用 pg_dump 生成但无 --clean,脚本不含 DROP。恢复到已有数据的库时,
   CREATE TABLE 全部 'already exists',COPY public.posts 在第一行就因主键
   冲突中止(COPY 0),被软删的文章(deleted_at 非空)永远回不来。

2. psql 默认不带 ON_ERROR_STOP,即使满屏 ERROR 退出码仍是 0。run_restore
   只判断 status.success(),于是 tasks::update 误报 TaskStatus::Done——
   用户看到「恢复完成」但实际零写入。

3. 即使数据写进去,前端 moka 缓存(posts/tags/stats/search)和 SSR 世代号
   也不会失效,恢复后仍读旧数据。

修复:
- run_pg_dump_backup 加 --clean --if-exists,备份脚本自带 DROP IF EXISTS,
  恢复变为幂等的先删后建。
- run_restore 给 psql 加 -v ON_ERROR_STOP=1,任何 SQL 错误立即退出(码 3),
  status.success() 不再误判。
- 恢复成功后调用 invalidate_all_post_caches + invalidate_search_results
  + bump_global_generation,前端立即看到恢复的数据。

验证:建干净测试库跑完整反馈回路(3 篇 → 软删 2 篇 → 恢复),修复前 alive=1,
修复后 alive=3,exit 0。旧备份文件(无 DROP)在 ON_ERROR_STOP 下会正确报失败
而非假成功,需重新创建备份才能恢复。

根因假设(已通过最小反馈回路证实):pg_dump 无 --clean + psql 无 ON_ERROR_STOP +
缓存未失效三层叠加。hypothesis 来自 backup.rs 源码 + backups/*.sql 实际结构 +
psql 退出码语义实测。
2026-07-13 15:57:34 +08:00
xfy
1584e01425 chore(deps): update all cargo and pnpm dependencies to latest versions 2026-07-09 18:12:54 +08:00
xfy
6d0dd8bb6e feat(sql-console): SqlResultTable 组件骨架 + 模块挂载 2026-07-06 10:30:30 +08:00
xfy
ab05c01f76 test(api/database): 备份恢复补单测 + 修复 backup_path 路径穿越漏洞
backup.rs 是恢复流程的安全关键路径,此前无任何单测。本次:

提取纯函数(便于单测,无行为变更):
- is_valid_backup_filename:文件名白名单校验,替换三处内联 Regex
- parse_backup_mode:从 -- mode: 行提取模式值
- has_valid_signature:首行签名头校验

新增 15 个单测,覆盖:
- 文件名白名单(正常/路径穿越/特殊字符三类边界)
- backup_path 路径穿越纵深防御
- 签名校验(精确/前导空白/非系统文件/空内容)
- 模式解析(pg_dump/sql-fallback/缺失/空值/多次)

修复一个真实的路径穿越漏洞(测试驱动发现):
backup_path("/etc/passwd") 之前返回 /etc/passwd 而非 backups/——
因为 [BACKUP_DIR, filename].collect::<PathBuf>() 在 filename 为绝对
路径时会丢弃 BACKUP_DIR 前缀(PathBuf 语义),随后的 components 检查
在错位的路径上运行而漏判。改为先校验 filename 自身的 components
(只允许 Normal 段),从根上杜绝穿越。

注:第一道白名单 is_valid_backup_filename 已拒绝含 / 的文件名,
故该漏洞实际不可达;但纵深防御必须健壮,本修复使其名副其实。
2026-07-02 17:37:07 +08:00
xfy
533cabb0b5 test(api/database): SQL 控制台护栏补表驱动单测 + 加固
sql_console.rs 是 SQL 控制台的安全关键路径,此前无任何单测。本次:

测试(新增 24 个,覆盖护栏 1+2 全分支):
- check_guards: DROP SCHEMA/CREATE DATABASE 绝禁、DROP/TRUNCATE/ALTER
  需确认、UPDATE/DELETE 无 WHERE 绝禁(且 confirm 不能救)、多语句短路
- is_absolutely_forbidden: 大小写不敏感、误拦检查、报错名
- statement_type_name / is_read_only: AST 变体映射
- 两条安全不变量测试(见下)

加固(测试驱动发现的两个真实缺口):
1. 字符串预检 contains() 改为 token 序列匹配 is_absolutely_forbidden()——
   防多空格/制表符/换行绕过 DROP/CREATE DATABASE。
2. check_guards 补 Statement::CreateDatabase 绝禁分支——此前 sqlparser
   能解析 CREATE DATABASE(可绕过字符串预检进 AST),但 AST 层无对应分支,
   字符串预检是其唯一防线。现在双重拦截。

并锁定两条安全不变量,防止 sqlparser 升级时悄悄回退:
- DROP DATABASE 不可解析 → 字符串预检是唯一防线
- CREATE DATABASE 可解析 → 双重防线都生效
2026-07-02 17:33:19 +08:00
xfy
5426458f4a fix(docs): resolve rustdoc broken links and dead_code warnings
Some checks failed
CI / check (push) Failing after 5m18s
CI / build (push) Has been skipped
doc 构建报 8 个 broken_intra_doc_links + dx 构建报 7 个 dead_code 警告。
两者根源相同:被引用的常量/类型/模块要么是 server-only(消费方都在
#[cfg(feature="server")] 块里),要么是跨 cfg 引用(host doc 构建看不到
wasm32-only 类型)。

dead_code:给 sql_console 的 MAX_ROWS/ABSOLUTELY_FORBIDDEN/GuardResult 与
backup 的 BACKUP_DIR/FILENAME_RE/BACKUP_SIGNATURE 加 #[cfg(feature="server")]
gate——这些都是安全护栏常量,消费方全在 server fn 体里,WASM 构建剥掉 fn 体后
常量即成死代码。sysinfo_sampler 的 WASM 桩 read_snapshot 加 allow(dead_code)
(与 codemirror_bridge 既有处理一致)。

rustdoc 链接:跨模块引用改全路径(crate::xxx),跨 cfg 引用(EditorHandle 在
wasm32 mod 内、host doc 构建不可见)转义为纯文本。

验证:cargo doc(0 warning)+ cargo check(default)+ wasm32 check 全部干净。
2026-06-30 17:42:52 +08:00
xfy
0b6a076d78 feat(database): use real COUNT(*) for small tables, fall back to estimate for large
row_estimate came from pg_class.reltuples, which is -1 for tables that
have never been ANALYZEd (the common case on a fresh/small DB), so the
admin table list showed -1 for every table — meaningless.

Replace row_estimate with row_count + row_count_estimated:
- total_size < 100MB: SELECT count(*) (real value, cost negligible on
  small tables; identifier quoted via PG escaping to stay injection-safe)
- total_size >= 100MB: fall back to reltuples estimate, flagged so the UI
  can prefix with ~

Frontend now shows real counts (1/2/14...) for small tables and ~N for
large ones; header copy updated to reflect the mixed source.
2026-06-30 13:57:37 +08:00
xfy
36168f26d7 fix(database): cast extract(epoch)::double precision in active-connections query
extract(epoch FROM now() - query_start) returns numeric (decimal) in
PostgreSQL, but the result was read as f64 via r.get(4). tokio-postgres
has no FromSql<f64> impl for the numeric type, so get_db_status panicked
at runtime with 'error deserializing column 4' (HTTP 500).

Cast to ::double precision (float8) to match query_duration_secs: Option<f64>.

Audited the remaining column mappings in status.rs to rule out further
type mismatches — all sound: reltuples::bigint->i64, native bigint size
columns->i64, last_vacuum/last_analyze timestamptz->DateTime<Utc>
(chrono feature enabled), schema_migrations.version TEXT->String,
count(*)::int->i32 (fixed previously).
2026-06-30 13:51:48 +08:00
xfy
116286f80f fix(database): cast count(*)::int in get_db_status to match i32 field
count(*) returns bigint (int8) in PostgreSQL, but the result was read as
i32 via conn_row.get(0). tokio-postgres's FromSql<i32> rejects an int8
column, so get_db_status panicked at runtime with
'error deserializing column 0' (HTTP 500).

Cast count(*)::int to int4, matching the adjacent setting::int column
and the total_connections/max_connections i32 struct fields. No other
queries in the file are affected: reltuples is already ::bigint -> i64,
size columns are native bigint -> i64, and system_status.rs reads its
count(*) as i64 correctly.
2026-06-30 13:47:24 +08:00
xfy
ecf3c73715 fix(database): make src/api/database compile under WASM (web-only) build
The /admin/system feature was never verified against the WASM frontend
target (dx serve builds src/main.rs with --features web and no server).
The entire src/api/database/ tree unconditionally pulled in server-only
crates (axum, sqlparser, dashmap, tokio, tokio_postgres, futures, regex)
and server-gated helpers (get_current_admin_user, parse_session_token,
get_user_by_token), so the frontend build failed with 43 errors.

Root cause + fixes (mirror the established settings.rs pattern):

- Gate server-only  imports behind #[cfg(feature = server)] in
  status/system_status/sql_console/schema/tasks/backup. The #[server] macro
  strips call sites but NOT use statements, so unresolved imports remained.
- Ungate  in main.rs: SystemSnapshot is a shared serde
  type referenced by ServerStatus.host on both targets; only the sampler
  task/SNAPSHOT static are internally server-gated.
- Gate  (pure Axum handler, zero WASM consumers) and the
  download_backup Axum handler in backup.rs.
- Move restore_backup's validation preamble (regex/backup_path/fs) inside
  the #[cfg(feature = server)] block; gate std::path/chrono::Utc imports.
- Add [[bin]] required-features=[server] for generate_highlight_css so the
  syntect-dependent build tool is skipped in web-only builds.

Genuine WASM bugs surfaced once the server bodies stopped masking them:

- system.rs: Closure import path (dioxus::prelude::wasm_bindgen ->
  wasm_bindgen, matching write.rs); setTimeout expects i32 not u32;
  editor_handle() -> editor_handle.read() (Signal is not Fn in 0.7).
- codemirror_bridge.rs: set_schema extern took &SqlSchema but SqlSchema is
  a serde type with no IntoWasmAbi. Changed both set_schema signatures to
  &JsValue; call site now serializes via serde-wasm-bindgen::to_value.
- system.rs: signals .set() inside spawn/use_future/onclick closures need
   bindings (incl. the *_f rebinding copies); added cfg_attr
  allow(unused_mut) on the 4 tab components, matching write.rs convention.

Verified: cargo check (default/server) clean; cargo check --features web
--target wasm32-unknown-unknown clean (0 errors); dx check clean; 411
cargo tests pass.
2026-06-30 13:41:23 +08:00
xfy
37d7ec49eb fix(database): harden SQL console guards + review fixes
最终 code review 发现的安全/正确性修复:

- C1 (critical): execute_one 改用 stmt.to_string() 重序列化的单条语句 SQL
  而非原始整段 SQL,保证执行的语句与护栏检查的 AST 一致;杜绝 allow_multi
  时整段 SQL 被重复执行、读写分类与实际执行解耦。
- C2 (critical): check_guards 在 AST 层结构性禁止 DROP SCHEMA(ObjectType
  匹配),不再仅靠字符串预检——防 SQL 注释/空白绕过。
- I2: 备份/恢复在 DATABASE_URL 为空时提前失败任务,而非传空串给 pg_dump/psql。
- I4: 回退备份进度计算用 u32 避免 >255 表时截断/溢出。
- I5: SQL 导出用真实表名(table 模式)而非硬编码 "export"。
- M7: 移除 SqlResult 中未使用的 total_estimate 字段。
2026-06-29 19:24:06 +08:00
xfy
6e94db0c2b feat(admin): add backup/restore tab (dual-mode + task progress polling)
新增 dashmap 依赖。create_backup 探测 pg_dump 优先(含 schema,签名头前置)、
不可用回退纯 SQL(COPY TO STDOUT 逐表,按表精确进度)。restore_backup 校验
签名头(仅本系统备份)+ 路径穿越防护 + 二次确认,探测 psql 执行。
list_backups/delete_backup 管理备份,GET /api/database/backups/{name} 下载
(admin 鉴权 + 白名单)。tasks.rs DashMap 进度表(1h 惰性 GC),前端每 1.5s
轮询 get_task_progress 显示进度条/stage,完成后刷新列表。
backups/ gitignored 不直接暴露。AGENTS.md 同步新增 CodeMirror 子项目与
数据库管理章节。
2026-06-29 19:16:11 +08:00
xfy
5c4b2a53d1 feat(admin): add data export tab (Axum streaming, SQL/CSV)
新增 GET /api/database/export Axum 流式路由(镜像 upload.rs 的 cookie→admin
鉴权),支持按表/按查询导出。CSV 用 COPY ... TO STDOUT WITH CSV 流式
(大表不 OOM),SQL 逐行拼 INSERT(含类型化转义)。表名白名单 + 查询
AST 只读强制。前端 ExportTab 触发 window.open 下载。
2026-06-29 19:01:16 +08:00
xfy
a49f47c8a6 feat(admin): add SQL console tab (read-write + 4 guards + sqlparser)
新增 sqlparser 依赖(optional+server),AppError 加 BadRequest 变体(护栏
返回动态用户可见消息)。execute_sql server function 全读写执行,4 道护栏:
(1) sqlparser AST 高危语句闸门——DROP DATABASE/SCHEMA 字符串预检绝禁、
DROP/TRUNCATE/ALTER 需勾选「我了解后果」;(2) 无 WHERE 的 UPDATE/DELETE 拒绝;
(3) 复用 STATEMENT_TIMEOUT_SECS 超时上限;(4) 前端写操作二次确认。
默认禁多语句,结果 500 行截断。get_db_schema 拉表/列供 CodeMirror 补全。

前端 SqlConsoleTab:CodeMirror 编辑器(Vim + Catppuccin 主题跟随站点 +
实时 schema 补全)+ 选项 toggles + 结果表格 + EXPLAIN 输出。
2026-06-29 18:56:15 +08:00
xfy
c4c490b881 feat(admin): add server status tab (sysinfo host metrics + cache hit rate)
新增 sysinfo 依赖(optional+server),sysinfo_sampler 后台采样任务
(SYSINFO_SAMPLE_SECS 可配,默认 0.5s)周期刷新 CPU/内存/磁盘/load 到 RwLock
快照,get_server_status 只读快照零成本。cache.rs 给 9 个缓存加 AtomicU64
hit/miss 计数 + cache_stats() 聚合命中率。

前端 ServerStatusTab:应用内指标(运行时间/连接池/会话/CPU/内存/磁盘/load)+
缓存命中率表 + 刷新按钮 + 自动刷新开关(500ms/1s/2s/5s/手动,默认手动)。
2026-06-29 18:45:02 +08:00
xfy
7553dcf405 feat(admin): add database status tab (tables/connections/migration version)
get_db_status server function 查 pg_catalog 聚合:数据库总大小/连接数/
表清单(行数估算+大小+死元组)/索引占用 Top10/活跃连接(过滤自身)/迁移版本。
前端 DbStatusTab:概览卡片 + 表/索引/连接表格 + 刷新按钮 + 自动刷新开关
(1s/2s/5s/30s/手动,默认手动;wasm 端用 web_sys setTimeout 轮询,无新依赖)。
2026-06-29 18:36:08 +08:00