793 Commits

Author SHA1 Message Date
xfy
d1e08ec402 fix(image): write disk cache atomically via temp-file + rename
.dat/.ct 改为先写 .tmp 再 rename,避免并发请求读到内容与 content-type
错配的半成品文件(L5)。写失败或 rename 失败时清理临时文件与目标。
2026-06-18 13:40:48 +08:00
xfy
3723cd03f9 fix(image): add canonicalize prefix check to is_path_safe for defense in depth
子串检查之外,对已存在文件做 canonicalize 前缀校验,确认解析后路径仍在
uploads 目录内,抵御符号链接等绕过(L4)。文件或 uploads 目录不存在时
只靠第一层校验(交由后续读取报 404)。函数改为 async,调用点与测试同步更新。
2026-06-18 13:39:46 +08:00
xfy
7a6e9350fe fix(image): reject undecodable images with 422; cap raw file size at 20MB
- decode 失败(WebP 与其他格式)不再降级返回原始字节,防止构造的畸形文件以
  图片 content-type 返回任意内容(M3)
- 原始分支读前查 metadata,超 20MB 返回 413,避免超大文件撑爆内存
2026-06-18 13:38:03 +08:00
xfy
22e883c6d9 fix(image): add X-Content-Type-Options: nosniff to all image responses
304 与 200 两个分支都附加 nosniff,防止浏览器对 content-type 错配的图片字节
做 MIME sniff(M2)。配合原始文件分支按扩展名决定 content-type 的行为做纵深防御。
2026-06-18 13:36:39 +08:00
xfy
1e2e3c9332 perf(db): add statement_timeout; skip retry on pool Timeout errors
- 连接配置 statement_timeout(默认 30s,STATEMENT_TIMEOUT_SECS 可调),
  防慢查询长时间占用连接拖垮池(L6)
- get_conn 对 Timeout(池满)错误立即返回不再 sleep 重试,避免雪崩;
  仅 Backend/Postgres 错误才退避重试
2026-06-18 13:35:33 +08:00
xfy
179897ba6f fix(migrations): make all migrations idempotent; distinguish applied vs error
- 006: ADD COLUMN 补 IF NOT EXISTS
- 002: 5 个索引(status_published/slug_unique/post_tags_post/post_tags_tag/cover)补 IF NOT EXISTS
- migrate.sh: 区分「已应用」与「真出错」,真错误时打印输出并中止,不再静默吞掉(M6)

已验证:第二次运行全部 OK(幂等),不再出现重复对象错误。
2026-06-18 13:34:07 +08:00
xfy
71d4126e94 fix(posts): wrap rebuild_content_html in single transaction
整批 UPDATE 纳入事务,任一写入失败整批回滚,避免产生「部分文章已重建」的
中间态(M5)。渲染失败(spawn_blocking panic)仍跳过该条不进入事务操作。
失败时返回 rebuilt:0 明确告知整批回滚。
2026-06-18 13:32:55 +08:00
xfy
82ab190e0d fix(comments): make duplicate-check atomic with transaction; index content_hash
查重 SELECT 与 INSERT 包进同一事务,串行化并发请求缩小重复窗口(M4);
重复时 rollback 空事务。content_hash 加索引(非唯一,避免误杀不同作者
发相同短内容的合法场景)加速 5 分钟窗口查重,原先全表扫。
2026-06-18 13:31:37 +08:00
xfy
1c6974ca68 fix(auth): run dummy Argon2 verify for non-existent users to prevent timing enumeration
用户不存在时执行一次固定合法哈希的 verify(必然失败),抹平与「密码错误」
路径的时序差,防止通过响应时间枚举账号(L2)。dummy 哈希走 spawn_blocking,
与真实校验路径耗时一致;响应消息保持一致。
2026-06-18 13:29:55 +08:00
xfy
05c01ccebe fix(session): serialize session-limit enforcement with row lock
login 的 COUNT→DELETE→INSERT 改为事务内执行,并对 users 行加 FOR UPDATE,
串行化同一用户的并发登录,消除超出 MAX_SESSIONS_PER_USER 的竞态(M1)。

锁定 users 行而非 sessions 表,粒度最小;commit 后无残留 DB 操作。
2026-06-18 13:27:49 +08:00
xfy
53bfb1b7c0 feat(session): invalidate all sessions on role/status change via generation
users 表加 session_generation 列。get_user_by_token 缓存命中后回查 DB
generation,不匹配则逐出缓存并视为未登录,消除降级/封禁用户的权限残留
窗口(H2)。新增 invalidate_user_sessions 用于 bump generation,当前仓库
无运行时角色变更入口,作为未来用户管理功能的基础设施预留。

- 迁移 012:ADD COLUMN IF NOT EXISTS session_generation INT DEFAULT 0
- User/SessionUser 同步加字段,From<User> 如实传递
- 缓存校验走主键查询,亚毫秒级
2026-06-18 13:26:09 +08:00
xfy
82a3c12940 feat(security): add Origin-based CSRF protection for write endpoints
对所有 POST/PUT/PATCH/DELETE 请求校验 Origin(回退 Referer)等于本站,
堵住 login CSRF 与未来 GET 化写接口的盲区(SameSite=Lax 覆盖不到)。

- 新增 src/api/csrf.rs:纯函数 origin 解析(不引入 url crate)+ axum 中间件
- 挂载到 upload 路由与 Dioxus app 路由(最外层,先于超时/压缩)
- APP_BASE_URL 配置可信域名;未设置时回退 Host + X-Forwarded-Proto
- GET/OPTIONS 放行;拿不到本站 origin 时放行避免误杀
- 9 个单测覆盖写方法识别、origin 标准化、默认端口省略、头解析回退
2026-06-18 13:22:59 +08:00
xfy
00478e4a1a perf(rate-limit): use lenient bucket when client IP is unknown
TRUSTED_PROXY_COUNT=0(默认)时,Dioxus server function 拿不到 TCP 对端,
get_client_ip 返回 "unknown",所有匿名请求共用严格桶(1 req/s, burst 5),
正常用户的高频请求被误杀。check_strict_limit 现在对 unknown IP 改走
宽松桶(30 req/s, burst 100,可通过 RATE_LIMIT_UNKNOWN_* 调整)。
配好反向代理后走真实 IP,仍命中严格桶。新增 2 个 serial 测试锁定两路行为。
2026-06-18 11:28:06 +08:00
xfy
9986d1ce4e perf(auth): compile email regex once via LazyLock
auth.rs 与 comments/helpers.rs 原先每次校验都 Regex::new 重新编译。
改为 LazyLock 全局静态,正则只编译一次。
2026-06-18 11:25:15 +08:00
xfy
3d187382cc perf(sanitizer): staticize allowlists with LazyLock to avoid per-call allocation
default_allowed_tags / clean_content_tags / default_allowed_schemes
原本每次调用都新建 HashSet 并逐个 insert;sanitize() 还 clone 一份。
改为 LazyLock 静态集合,SanitizerConfig 直接持有 &'static 引用,
评论白名单 COMMENT_ALLOWED_TAGS 在默认集合上派生。18 个 sanitizer
测试全部通过,XSS/URL 过滤行为不变。
2026-06-18 11:23:30 +08:00
xfy
753525fb41 perf(upload): offload GIF/WebP raw image validation to spawn_blocking
GIF 走 image::load_from_memory 会完整解码动画帧,在 async 上下文阻塞
worker。移到 spawn_blocking,JoinError 兜底任务失败。
2026-06-18 11:19:34 +08:00
xfy
033b89ccb8 perf(posts): offload Markdown rendering to spawn_blocking
create/update/rebuild 三处 Markdown 渲染(含 syntect 高亮)移到阻塞
线程池。rebuild 用 spawn_blocking 的 JoinError 替代 catch_unwind 捕获
渲染 panic,避免单条记录拖垮整批。
2026-06-18 11:18:36 +08:00
xfy
45e92795de refactor(markdown): unify parser options across TOC and HTML passes
原先第一遍用 Options::all() 收集 heading,第二遍只用 ENABLE_TABLES
生成 HTML,两遍对扩展语法的处理不一致。统一为 Options::all()。
2026-06-18 11:16:58 +08:00
xfy
62600a6687 perf(auth): offload Argon2 hash/verify to spawn_blocking
Argon2 是 memory-hard 计算,登录/注册时同步执行会阻塞 Tokio worker
数百毫秒。改为在阻塞线程池执行,JoinError 兜底任务 panic。
2026-06-18 11:16:13 +08:00
xfy
79cb809010 perf(posts): add optional pagination to get_posts_by_tag, fix total count
- get_posts_by_tag 现接受 page/per_page 可选参数;两者均 None 时返回全部
  (上限 200,用于无翻页 UI 的标签详情页),均提供时走标准分页。
- 修正 total:不再用 posts.len(),改为真实 COUNT(*),即使被 LIMIT 截断
  也返回完整计数。
- 新增 CacheKey::PostsByTagPage 分页缓存键,与不分页键 PostsByTag 共存。
- 前端 tags.rs 传 (None, None) 保持原全部展示行为。
2026-06-18 11:14:56 +08:00
xfy
2dda168b19 perf(db): switch pool recycling method from Verified to Fast
Verified 每次取连接都会额外发 SELECT 1 验证;Fast 直接复用,
依赖 tokio-postgres 在使用时自然报错,由 get_conn 的退避重试兜底。
2026-06-18 11:08:14 +08:00
xfy
54f7bd481a perf(db): replace fixed 2s retry with exponential backoff + jitter
get_conn 现在按 base*2^attempt + 随机 jitter 退避(200ms 起步),
避免连接池抖动时多请求同步重试形成惊群。
2026-06-18 11:06:57 +08:00
xfy
104ff427f9 perf(db): add exponential backoff retry helper with tests
引入 src/db/retry.rs,提供 base * 2^attempt + jitter 的退避序列,
取代 pool.rs 中固定 2s 重试。MAX_RETRIES 将在下一个提交中被 get_conn 使用。
2026-06-18 11:05:22 +08:00
xfy
c6161764a9 perf(db): add indexes for trash and admin post list queries
- idx_posts_deleted_at: 回收站查询 WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC
- idx_posts_created_at_admin: 管理后台全量列表 WHERE deleted_at IS NULL ORDER BY created_at DESC
2026-06-18 11:02:13 +08:00
xfy
306da3cf83 Merge caching and SSR invalidation improvements
Some checks failed
CI / check (push) Failing after 24m9s
CI / build (push) Has been skipped
2026-06-18 10:43:40 +08:00
xfy
ed589f9c79 docs(changelog): record caching and SSR invalidation improvements 2026-06-18 10:37:14 +08:00
xfy
668920e9fb docs(ssr): clarify that generation counters are future-ready infrastructure 2026-06-18 10:30:18 +08:00
xfy
7f372446da refactor(ssr): restrict X-SSR-Generation header to GET requests and add serial tests 2026-06-18 10:30:12 +08:00
xfy
3ee39d910c refactor(ssr): remove unused per-slug/per-tag generation counters 2026-06-18 10:30:07 +08:00
xfy
4c695b4fc3 refactor(tasks): use named constants for MB and hours in cleanup 2026-06-18 10:11:07 +08:00
xfy
12355b7859 fix(tasks): skip symlinks during image disk cache cleanup 2026-06-18 10:10:51 +08:00
xfy
a71da7473d perf(image): remove redundant Vec clone before spawn_blocking 2026-06-18 10:10:44 +08:00
xfy
c0b14ed498 refactor(image): make bytes dependency optional and server-gated 2026-06-18 10:10:40 +08:00
xfy
f1e5b657a3 chore(env): document image disk cache limits in .env.example 2026-06-18 10:03:42 +08:00
xfy
0b107c3f2e feat(tasks): add periodic image disk cache cleanup 2026-06-18 10:03:38 +08:00
xfy
36554af5f5 perf(image): store cached image data as Bytes to avoid Vec cloning 2026-06-18 10:03:35 +08:00
xfy
411e565465 docs(cache,search): update module and function comments 2026-06-18 09:55:45 +08:00
xfy
24bc6f44a0 fix(tasks): invalidate session cache after cleaning expired sessions 2026-06-18 09:55:04 +08:00
xfy
518b4e5d64 refactor(auth): store SessionUser instead of full User in session cache 2026-06-18 09:55:00 +08:00
xfy
c780247d17 feat(posts): invalidate search cache on writes 2026-06-17 17:21:01 +08:00
xfy
c40a771989 feat(search): cache search results with short TTL 2026-06-17 17:20:54 +08:00
xfy
1d216faa2f feat(auth): add in-memory session cache 2026-06-17 17:20:47 +08:00
xfy
75b8f80631 fix(posts): wrap empty_trash in transaction and lock rows for precise invalidation 2026-06-17 17:05:07 +08:00
xfy
05b9a3a595 feat: enable all HTTP compression algorithms by default
Some checks failed
CI / check (push) Failing after 14m38s
CI / build (push) Has been skipped
- COMPRESSION_ALGORITHMS defaults to 'all' when unset

- Support 'none'/'off' to explicitly disable compression

- Add parse_compression_algorithms helper and unit tests

- Update .env.example comment to reflect new default
2026-06-17 17:03:17 +08:00
xfy
7bd02d0ea9 perf(cache): run tag post invalidations concurrently 2026-06-17 16:59:29 +08:00
xfy
2d8f1e0d98 refactor(posts): avoid redundant cache invalidation in bulk fallback paths 2026-06-17 16:59:24 +08:00
xfy
c77f751377 refactor(posts): apply precise invalidation limit to batch_restore_posts 2026-06-17 16:59:18 +08:00
xfy
ca212a2aab fix(posts): lock rows and read metadata in transaction for delete/purge/restore 2026-06-17 16:58:54 +08:00
xfy
634f733e36 feat: add Cache-Control headers for public pages and static assets
- Public SSR pages: public, max-age=300, stale-while-revalidate=3600

- Static assets (/_dioxus/, /wasm/, *.js, *.wasm, style.css, highlight.css): public, max-age=31536000, immutable

- API, admin, auth pages and non-GET/HEAD requests remain uncached

- Add unit tests for cache control path matching
2026-06-17 16:57:42 +08:00
xfy
1092fbb3ce fix(posts): ensure rebuild invalidates tag posts cache 2026-06-17 16:44:32 +08:00