51 Commits

Author SHA1 Message Date
xfy
0fe0387ea3 feat(admin/posts): 文章列表操作列新增「重建内容」按钮
新增 rebuild_post_content_html(post_id) server function,从 DB 读取
content_md(事务内 FOR UPDATE 锁行)重新渲染 content_html/toc_html
并更新 word_count/reading_time,按影响范围精准失效列表、slug 单篇
与搜索缓存。

PostRow 操作列在「编辑」「删除」之间插入「重建内容」按钮(鼠尾草绿
文字样式),按行禁用并切换「重建中...」文案,成功/失败弹出浏览器
提示,与删除按钮的反馈模式一致。

为避免与单篇 rebuilding 信号冲突,将原批量重建工具条信号 renaming
为 batch_rebuilding。
2026-07-03 10:13:15 +08:00
xfy
373498870a style: apply cargo fmt to workspace
Some checks failed
CI / check (push) Failing after 5m35s
CI / build (push) Has been skipped
2026-06-29 13:47:40 +08:00
xfy
60c51f44ab fix(posts): move rebuild SELECT into transaction with FOR UPDATE
Review 发现:SELECT 在事务外读 rows,事务内 UPDATE 时若并发编辑了该 post,
会用旧 content_md 覆盖新内容(非可重复读)。改为 SELECT 移入事务并加
FOR UPDATE,锁住待处理行直到 UPDATE 完成,消除丢失更新。

权衡:FOR UPDATE 锁最多 500 行直到事务结束,阻塞并发编辑;rebuild 是 admin
运维操作,可接受。
2026-06-18 14:17:20 +08:00
xfy
b34803e57d fix(search): drop ineffective trgm GIN index; correct misleading comment
ILIKE '%...%' 双侧通配符无法命中 trgm GIN(仅前缀模式命中),索引建了等于
白建且误导(L1)。删除以避免误导,搜索暂靠 LIMIT + 限流兜底;tsvector 全文
检索作为后续独立升级。
2026-06-18 13:42:23 +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
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
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
3ee39d910c refactor(ssr): remove unused per-slug/per-tag generation counters 2026-06-18 10:30:07 +08:00
xfy
411e565465 docs(cache,search): update module and function comments 2026-06-18 09:55:45 +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
75b8f80631 fix(posts): wrap empty_trash in transaction and lock rows for precise invalidation 2026-06-17 17:05:07 +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
1092fbb3ce fix(posts): ensure rebuild invalidates tag posts cache 2026-06-17 16:44:32 +08:00
xfy
c03093fc8b refactor(posts): use precise cache invalidation in rebuild 2026-06-17 16:37:20 +08:00
xfy
bb34e2c36e refactor(posts): use precise cache invalidation in trash operations 2026-06-17 16:37:20 +08:00
xfy
82b070e7ac refactor(posts): use precise cache invalidation in create/update/delete 2026-06-17 16:37:20 +08:00
xfy
15e7e2578d refactor(utils): extract reading_time helper 2026-06-17 16:25:22 +08:00
xfy
9f41ea7354 refactor(posts): remove orphan row_to_post_list and use row_to_post_full for get_post_by_id 2026-06-17 16:24:38 +08:00
xfy
1eedab8f21 perf(posts): remove content_md from list/search SQL, read stored word counts 2026-06-17 16:06:27 +08:00
xfy
a6f08d5d3f feat(db): add word_count and reading_time columns to posts 2026-06-17 16:06:21 +08:00
xfy
51e20980db perf(stats): combine three COUNT queries into one conditional aggregation 2026-06-17 15:51:29 +08:00
xfy
170c021b37 feat(cache): add PostListItem DTO and use it in list/tag/search caches 2026-06-17 15:51:19 +08:00
xfy
449a545886 security: fix critical issues from repository review
Some checks failed
CI / build (push) Has been cancelled
CI / check (push) Has been cancelled
P0 blockers:
- Fix migration numbering conflict and duplicate indexes
- Change comments.post_id FK to ON DELETE CASCADE
- Restrict public post detail endpoint to published posts only
- Fix rate-limiting IP extraction and fallback to ConnectInfo
- Harden HTML sanitizer: deny unknown URL schemes, restrict data URIs
- Remove session token from login response body
- Enforce image pixel/dimension limits on upload and serving

P1 high-risk:
- Validate uploads by magic bytes and decode GIF/WebP
- Add pagination/rate-limiting to search, tag posts, and comments
- Make first-admin registration and slug uniqueness check atomic
- HTML-escape comment author fields
- Improve HTML minify cache key and skip admin/error responses
- Add mobile navigation menu

P2 accessibility/quality:
- Associate form labels with inputs
- Key PostDetail article by slug to re-init scripts on navigation
- Improve image viewer keyboard accessibility
- Make theme toggle SSR-friendly and add aria-label
- Invalidate slug 404 cache on create and pending count on new comment
- Deduplicate tags case-insensitively

P3 cleanup:
- Remove unused tower-http dependency, expand make clean
- Configure DB pool timeouts and verified recycling
- Run background cleanup tasks immediately on startup
- Use SHA-256 for stable disk cache keys
- Log DB errors with Display instead of Debug
- Update README migration instructions

All tests pass (321), clippy clean, dx check clean.
2026-06-17 10:34:14 +08:00
xfy
cafbddb861 refactor: replace server-only dead_code allows with cfg(feature = "server")
Replace #[allow(dead_code)] on server-only helpers with #[cfg(feature = "server")]
to make the server/WASM split explicit and avoid compiling unused server logic
into the WASM frontend.

- Gate password/session/auth/comment helpers and model parsers with server feature
- Gate related imports and tests accordingly
- Remove genuinely unused CreatePostRequest and CreateCommentRequest
- Keep #[allow(dead_code)] for true dead code (stub methods, unused public APIs)
- Use #[cfg(any(target_arch = "wasm32", test))] for THEME_KEY
- Update AGENTS.md note
2026-06-16 16:45:08 +08:00
xfy
b6de75ee97 feat(api): 新增 list_deleted_posts 回收站列表接口 2026-06-16 12:34:28 +08:00
xfy
65e090da27 feat(api): 新增回收站恢复/彻底删除/批量/清空接口 2026-06-16 12:31:55 +08:00
xfy
054d7450f7 feat(model): Post 新增 deleted_at 字段并用 try_get 兼容现有查询 2026-06-16 12:29:37 +08:00
xfy
863ec5ea52 fix(posts): get_post_by_slug 的 SELECT 补上 toc_html 列
row_to_post_full 用 row.get("toc_html") 读取该列,但 get_post_by_slug
的 SQL 漏选了 p.toc_html,导致访问公开文章详情时 panic(invalid column
toc_html)。补上列即可。
2026-06-16 11:34:12 +08:00
xfy
c60e4e08d7 fix(build): 用 cfg gate 消除 WASM 构建的 27 个假阳性 warning
Some checks failed
CI / check (push) Failing after 5m17s
CI / build (push) Has been skipped
Dioxus fullstack 在 WASM 构建中会剥离 #[server] 函数体,导致仅在
函数体内使用的导入/函数/常量被误报为 unused / dead_code。

- 仅在服务端使用的导入加 #[cfg(feature="server")]:get_conn、
  PostStatus/PostStats/Tag、password/session、User/UserRole、
  slug.rs 的 dioxus::prelude(保留 ServerFnError 给 server-gated
  的 ensure_unique_slug)
- 仅在服务端使用的项加 cfg gate:clamp_pagination 及其常量、
  rebuild 常量、format_relative_time、compute_content_hash、
  CommentStatus::from_str
- CommentCountResponse/PendingCommentsResponse 出现在 #[server]
  签名中(WASM 保留),无法 gate,改用 #[allow(dead_code)],
  与现有 CreateCommentRequest 处理一致

make build 零 warning,cargo test 299 passed。
2026-06-16 11:29:18 +08:00
xfy
1cc4bc5aeb style(list): 简化多行表达式为单行
Some checks failed
CI / check (push) Failing after 13m53s
CI / build (push) Has been skipped
2026-06-15 14:15:11 +08:00
xfy
eaa7118e09 fix(posts): 钳制 page 上限,防止无界 OFFSET 与缓存键扇出
clamp_pagination 原先仅限制 per_page,page 无上限。攻击者可用海量不同
page 值撑大缓存键空间(缓存污染)并触发无意义的超大 OFFSET 扫描。新增
MAX_PAGE=10_000,对任何实际博客都足够宽裕(配合 MAX_PER_PAGE 最多覆盖
50 万篇文章),同时把缓存键空间限制在有限范围。
2026-06-15 11:38:24 +08:00
xfy
cfa4975813 fix(posts): 钳制分页参数 per_page 上限,消除公开接口 DoS
list_published_posts 为无需认证的公开接口,原先 per_page 直接透传给
SQL LIMIT,攻击者可传入巨大值迫使数据库扫描并实例化超大 Vec,造成
内存放大与拒绝服务。新增 MAX_PER_PAGE=50 与 clamp_pagination(),在
list_published_posts 与 list_posts 入口钳制 page/per_page;钳制值同时
用于缓存键与查询,避免同一逻辑页落入不同缓存条目。
2026-06-15 11:27:25 +08:00
xfy
f7288cc390 style: 格式化测试代码以符合 clippy 宽度限制 2026-06-15 10:40:00 +08:00
xfy
a785683fc6 docs(posts): 补充中文注释 2026-06-12 18:50:17 +08:00
xfy
4fe26f7eb3 test: improve unit test coverage and assertions
Some checks failed
CI / check (push) Failing after 15m51s
CI / build (push) Has been skipped
- Add tests for sanitizer, mime_to_ext, clean_tags, Theme::toggle
- Tighten assertions in highlight, markdown, post status classes
- Add boundary and XSS cases for comments, slug, rate_limit, webp
- Update Cargo.lock for serial_test dev-dependency
2026-06-12 18:13:51 +08:00
xfy
942ac853fe refactor: tighten module-level allow attributes 2026-06-12 17:26:46 +08:00
xfy
294d60afab style: format rust code
Some checks failed
CI / build (push) Has been cancelled
CI / check (push) Has been cancelled
2026-06-12 17:14:31 +08:00
xfy
ee6aaf179c feat(posts): add batch limit and error handling to rebuild_content_html 2026-06-12 11:44:53 +08:00
xfy
2e9d123396 feat(posts): add rebuild_content_html server function
New admin-only server function that re-renders content_html and
toc_html for posts from their content_md:
- rebuild_all=false: only processes posts where content_html IS NULL
- rebuild_all=true: processes all non-deleted posts
- Processes rows one at a time to avoid table locks
- Invalidates post list and stats caches after rebuild
2026-06-12 10:35:59 +08:00
xfy
a975e45cc9 feat(posts): store toc_html in DB on create and update
- create_post: include toc_html in INSERT alongside content_html
- update_post: include toc_html in UPDATE alongside content_html
- Both write toc_html as NULL when empty, Some(..) otherwise
2026-06-12 10:35:51 +08:00
xfy
acd85d18ce perf(posts): read pre-rendered content_html/toc_html from DB instead of re-rendering
Previously row_to_post_full() ignored the stored content_html and
re-rendered markdown with render_markdown_enhanced() on every read,
triggering expensive ammonia sanitization (aho_corasick accounted for
87% CPU in flame graph).

Now reads content_html and toc_html directly from the database row.
Falls back to render_markdown_enhanced() only for legacy posts where
content_html is NULL.
2026-06-12 10:35:39 +08:00
xfy
04737300e6 feat(comments): add complete comment system with guest commenting, moderation, and admin UI
Implements a fully self-built comment system for the blog:

Data layer:
- comments table with BIGSERIAL PK, parent_id self-reference (ON DELETE SET NULL),
  depth tracking (max 20), status workflow (pending/approved/spam/trash),
  content hashing for dedup, GDPR consent tracking, IP/UA storage with auto-purge
- 5 partial indexes optimized for read patterns
- updated_at auto-trigger

API (9 Dioxus server functions):
- Public: get_comments, get_comment_count, create_comment
- Admin: get_pending_comments, get_pending_count, get_all_comments,
  approve_comment (with ancestor auto-approval), spam_comment, trash_comment,
  batch_update_comment_status

Security:
- Function-level rate limiting (1/sec, burst 5) via FullstackContext IP extraction
- Input validation (name, email, URL scheme, content length, consent)
- Parent chain validation (must be approved, same post)
- Strict comment Markdown renderer (headings→strong, no img/id/data URIs, nofollow links)
- Honeypot anti-spam field
- 5-minute dedup window via SHA-256 content hash

Frontend:
- CommentSection with SuspenseBoundary isolation
- Flat-list rendering with depth-based CSS indentation (responsive)
- Gravatar via cravatar.cn (server-computed, email never exposed)
- Inline reply forms (one-at-a-time via Signal)
- Admin action buttons (approve/spam/delete) visible per-comment
- CommentForm with privacy consent, Markdown hint, loading states

Admin:
- /admin/comments page with status tabs, batch operations, pagination
- Pending count badge on admin dashboard

Infrastructure:
- Shared get_current_admin_user moved from posts/helpers to auth module
- COMMENT_LIMITER rate limiter tier
- Moka caches (60s TTL for comments, 10s for pending count)
- IP/UA purge background task (daily, 90-day retention)
2026-06-11 12:34:26 +08:00
xfy
265eb15887 feat(admin): add pagination to posts management page
- list_posts API now accepts page/per_page params with LIMIT/OFFSET
- Add /admin/posts/:page route with PostsPage component
- Pagination component with prev/next navigation (20 posts/page)
- Update dashboard to use new list_posts(1, 5) signature
2026-06-10 18:03:36 +08:00
xfy
5e2487a067 fix: gate helper imports with #[cfg(feature = "server")] in posts API
Server-only helper functions (get_current_admin_user, row_to_post_list,
row_to_post_full, sync_tags, clean_tags) are defined behind
#[cfg(feature = "server")] in helpers.rs, but their imports in consumer
files were not gated, causing WASM build failures.
2026-06-10 14:49:54 +08:00
xfy
311ddbe204 perf(cache): cache COUNT(*) result separately to avoid redundant queries
- Add TotalPublishedPosts cache key for reusing total count across pages
- list_published_posts now checks total cache before running COUNT(*)
- Add note to get_posts_by_tag about total = posts.len() assumption
- Remove unused invalidate_total_published_posts helper
2026-06-10 14:44:53 +08:00
xfy
44eba24121 feat: return total count from post list server functions 2026-06-10 14:14:04 +08:00
xfy
72c1efa566 feat: add total field to PostListResponse 2026-06-10 13:57:06 +08:00