165 Commits

Author SHA1 Message Date
xfy
f5e19def1a feat(api): 新增回收站配置读写接口 2026-06-16 12:34:28 +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
a2ccc3da1a Merge branch 'fix-dos-and-toc-xss'
安全修复:
- 钳制公开分页接口 per_page 与 page 上限,消除 DoS 与缓存键扇出
- 修正 TOC 标题在属性上下文的转义,防止属性注入(XSS)

经 code review 后追加:
- 复用既有 escape_html,删除重复转义实现
- 增加 page 上限作为纵深防御
2026-06-15 11:40:01 +08:00
xfy
a3d6c2f19e refactor(markdown): 复用既有 escape_html,删除重复的转义实现
hooks::comment_storage::escape_html 已实现完整 OWASP 转义(& < > " '),
且无 server feature 门控,全平台可用。escape_heading_text 改为委托给它,
避免在仓库内维护第二份行为略有差异(缺 >)的转义函数。补充 < 转义测试。
2026-06-15 11:38:24 +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
6d32664020 fix(markdown): 转义 TOC 标题文本,防止属性上下文注入
generate_toc_html 原先用 clean_html 处理标题后拼进 aria-label="..." 与
<a> 正文,但 clean_html 只做正文 HTML 消毒、不转义双引号。标题形如
" onmouseover="alert(1) 会越出属性边界。新增 escape_html_attr(),对
标题文本统一做属性上下文转义(& " <),正文与属性两处一致使用。

内容由 admin 写入、严重度中低,但属真实消毒逻辑缺口,TOC 会展示给所有读者。
2026-06-15 11:27:32 +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
263208b930 style: 修正新测试的格式以符合 rustfmt 宽度限制
cargo fmt 自动调整了 highlight.rs 与 error.rs 中新增测试的
assert! / assert_eq! 长行折行。
2026-06-15 11:22:35 +08:00
xfy
9fc626232c test(error): 补全 AppError 各变体的消息转换测试
原仅覆盖 Unauthorized/Forbidden/NotFound/DbConn/Query 五个变体,
补充:
- Internal 变体的消息透传
- Transaction 变体的 SQL 细节隐藏(与 Query 行为一致)
- 三类数据库错误统一返回中文通用提示的聚合断言

共 8 个测试,全部通过。
2026-06-15 11:20:33 +08:00
xfy
28f0117f06 test(sanitizer): 补充 is_safe_url 分支测试
is_safe_url 是 HTML 消毒器的安全核心,原仅通过 clean_html 间接覆盖。
新增 9 个直接单元测试,锁定各分支契约:

- https/http 白名单通过
- javascript / vbscript scheme 拒绝(含大小写混淆)
- data URI 按 allow_data_uri 标志决定(文章正文 vs 评论)
- 相对路径与锚点片段通过
- 空字符串/纯空白视为安全
- mailto / tel / ftp 等其它白名单 scheme 通过
- 含空白的 scheme 名(混淆手法)被拒绝

共 17 个测试,全部通过。
2026-06-15 11:19:46 +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
9921f8eebf docs(comments): 补充中文注释 2026-06-12 18:42:19 +08:00
xfy
26b012c40c docs(api, auth): 补充中文注释 2026-06-12 18:27:24 +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
71ac08c373 feat(rate_limit): derive real client IP from X-Forwarded-For with TRUSTED_PROXY_COUNT 2026-06-12 17:13:17 +08:00
xfy
4ae7b38131 feat(auth): add Secure flag to session cookie via COOKIE_SECURE env 2026-06-12 17:02:22 +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
a10bf8737c refactor(sanitizer): extract shared sanitizer module and migrate from ammonia to lol_html 2026-06-12 11:15:42 +08:00
xfy
f6589121da perf(markdown): eliminate duplicate clean_html call in generate_toc_html
clean_html() triggers expensive ammonia sanitization with aho_corasick.
Previously called twice for the same text (aria-label + display text).
Now calls once and reuses the result.
2026-06-12 10:36:06 +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
bd9e87128d perf(ssr): optimize request throughput by 32%
- Cache ammonia::Builder with LazyLock (was rebuilt per request)
- Enable tracing release_max_level_info to strip tracing overhead at compile time
- Remove TraceLayer and tower-http trace feature from production
- Increase DB pool size 10→20 (configurable via DB_POOL_SIZE)
- Increase SSR cache TTL 300s→3600s (configurable via SSR_CACHE_SECS)

Benchmark: 7,444 → 9,840 req/s, P99 latency 27.6ms → 11.1ms
2026-06-12 09:36:53 +08:00
xfy
569eec5bf8 fix(admin): 评论管理页多项修复
- 修复默认头像无法显示:AdminComment 添加 avatar_url 字段
- 修复状态标签换行:移除 w-20 固定宽度,添加 whitespace-nowrap
- 操作按钮添加 cursor-pointer
- 已处于该状态的评论隐藏对应操作按钮
- 修复操作后页面不自动刷新:将 restart() 移到异步块内
2026-06-11 16:10:42 +08:00
xfy
7b3d894e96 feat(api): add CheckPendingStatus server function
Accepts Vec<i64> of comment IDs, returns their current status.
IDs not found in DB return status 'gone'. Empty vec returns early.
Used by client to prune localStorage pending comments that are
no longer pending (approved/spam/trash/deleted).
2026-06-11 14:18:49 +08:00
xfy
d63cee58c2 feat(api): return comment_id, avatar_url, depth from create_comment
- Add comment_id/avatar_url/depth Option fields to CommentResponse with serde default
- Extract RETURNING id from INSERT in create.rs
- Compute gravatar_url server-side (md5 not available in WASM)
- Return computed depth for correct client-side pending comment indentation
- All error paths return None for all new fields
2026-06-11 14:13:14 +08:00
xfy
59d5b9222a refactor(comments): remove privacy consent checkbox and consented param 2026-06-11 13:22:57 +08:00
xfy
358dff152d fix(error): use Debug formatting in AppError logs for full error chain visibility 2026-06-11 13:19:18 +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
ccd564312b feat: hash session tokens and enforce configurable session limit per user 2026-06-11 09:45:56 +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
xfy
b1385efd5e refactor(api): split posts.rs into module directory
- Move posts.rs (891 lines) to posts/ with 11 submodules
- Extract shared tag sync logic into helpers::sync_tags
- Extract tag cleaning into helpers::clean_tags
- All external import paths unchanged via mod.rs re-exports
- Zero behavioral changes, pure structural refactor
2026-06-10 13:45:21 +08:00
xfy
2574e1e315 fix(api): gate AppError helper methods behind server feature
tracing is an optional dependency unavailable in WASM builds.
The constructors are only called from server-side code anyway.
2026-06-10 13:21:37 +08:00
xfy
bf401e4916 fix(api): correct AppError variant semantics for transaction/query errors
- client.transaction() failures: db_conn -> tx (logs correct category)
- auth session INSERT: db_conn -> query
- delete_post UPDATE: tx -> query (no transaction context)
2026-06-10 13:19:57 +08:00
xfy
a3fa602df2 refactor(api): unify error handling with AppError enum
Replace scattered ServerFnError::new("...") inline calls and utils.rs
helper functions with a single AppError enum that provides:
- Automatic tracing::error logging for system errors
- Safe generic messages to frontend (no SQL/connection detail leaks)
- Typed error variants (Unauthorized, Forbidden, NotFound, etc.)
- 5 unit tests for message passthrough and detail hiding

Removes src/api/utils.rs entirely; all error creation now goes through
src/api/error.rs.
2026-06-10 13:16:06 +08:00
xfy
4c534eeb4c fix(posts): invalidate tag and old slug caches on create/update 2026-06-09 17:15:29 +08:00
xfy
91caf3f8bd feat(posts): invalidate caches on write operations 2026-06-09 17:09:17 +08:00
xfy
887aacb5f1 fix(posts): check cache before auth, skip caching None slugs 2026-06-09 17:06:53 +08:00