对所有 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 标准化、默认端口省略、头解析回退
8.3 KiB
AGENTS.md
Development Commands
make dev # tailwindcss watch + dx serve (needs PostgreSQL)
make build # build-editor → highlight-css → tailwindcss → dx build --release
make build-linux # same as build but targets x86_64-unknown-linux-musl
make css # one-shot CSS
make css-watch # watch mode
make test # cargo test
make clean # cargo clean + rm public/style.css
Build order matters: make build runs build-editor → highlight-css (cargo run --bin generate_highlight_css) → tailwindcss --minify → dx build --release. Do not run dx build --release alone.
Prerequisites
- Rust 1.95+ with
wasm32-unknown-unknowntarget dxCLI (cargo install dioxus-cli)tailwindcssCLI v4 — install vianpm install -g @tailwindcss/cli(v4 splits the CLI into its own package; thetailwindcsscore package has nobin), or use the standalone binary- PostgreSQL running locally
Environment
Create .env (not committed):
DATABASE_URL=postgres://postgres:postgres@localhost:5432/yggdrasil
RUST_LOG=info
Optional tuning via env vars (all have sane defaults):
WEBP_QUALITY=85.0 # 0.0–100.0, clamped
WEBP_METHOD=2 # 0–6, clamped
RATE_LIMIT_STRICT_PER_SEC=1
RATE_LIMIT_STRICT_BURST=5
RATE_LIMIT_UPLOAD_PER_SEC=2
RATE_LIMIT_UPLOAD_BURST=15
RATE_LIMIT_IMAGE_PER_SEC=10
RATE_LIMIT_IMAGE_BURST=50
DB_POOL_SIZE=20 # database connection pool size
SSR_CACHE_SECS=3600 # incremental SSR cache TTL
Session / security tuning:
COOKIE_SECURE=false # set true/1/yes to add Secure flag to session cookie
TRUSTED_PROXY_COUNT=0 # number of reverse proxies in front of the app; used to extract real client IP from X-Forwarded-For
APP_BASE_URL= # e.g. https://your-domain.example — trusted origin for CSRF checks on write requests; unset falls back to Host header + X-Forwarded-Proto
Run migrations before first dev server start:
./migrate.sh # auto-creates DB, runs migrations/ in order
Architecture: Conditional Compilation
Dioxus 0.7 fullstack project with two independent gates — the most common source of compilation errors.
| Gate | Applies to | Used for |
|---|---|---|
#[cfg(feature = "server")] |
Server binary only | DB, env loading, background tasks, server function bodies, highlight, WebP, caching |
#[cfg(target_arch = "wasm32")] |
WASM frontend only | localStorage, DOM APIs, web_sys calls, theme detection |
Critical: Both default features (web + server) are enabled in Cargo.toml. The dx CLI handles feature selection during builds.
Stub pattern: src/db/mod.rs provides a DummyPool when server feature is disabled — do not remove.
Server-only helpers: src/auth/password.rs, src/auth/session.rs, src/api/auth.rs, src/api/comments/helpers.rs, and several model helper methods are gated with #[cfg(feature = "server")] because they are only called from server function bodies, which are stripped in WASM builds.
Server-only dependencies: Crates that are only used behind #[cfg(feature = "server")] (e.g., argon2, uuid, regex, pulldown-cmark, rand, http, sha2, hex, plus the pre-existing optional server stack) are declared as optional = true in Cargo.toml and enabled only through the server feature. They are not compiled into the WASM frontend.
Dual API Architecture
The server exposes two distinct API patterns:
-
Dioxus server functions (
#[server(Name, "/api")]insrc/api/) — auto-routed, callable from both client and server Rust. Spread acrosssrc/api/auth.rsandsrc/api/posts/. -
Axum routes (registered in
src/main.rs) — manualaxum::Routerfor endpoints that don't fit the server-function model:POST /api/upload— image upload (multipart, auth-required, rate-limited)GET /uploads/{*path}— image serving with on-the-fly resize/rotate/convert (query params:w,h,thumb,rotate,format,quality)
Server Module Structure
src/api/ — server functions + Axum handlers
auth.rs — login, register, session validation
markdown.rs — Markdown→HTML rendering (pulldown-cmark + ammonia sanitization)
image.rs — image serving with processing pipeline + disk+memory cache
upload.rs — image upload, auto-converts to WebP
rate_limit.rs — governor-based rate limiting (3 tiers: strict/upload/image)
slug.rs — URL slug generation
posts/ — CRUD server functions for blog posts
src/auth/ — password hashing (Argon2) + session token management
src/bin/ — generate_highlight_css (build-time CSS generation)
src/cache.rs — moka future-based caches for posts, tags, stats
src/components/ — Dioxus UI components
src/db/ — PostgreSQL pool (deadpool-postgres, LazyLock global)
src/hooks/ — shared Dioxus hooks
src/models/ — Post, User, Tag data models
src/pages/ — route page components (frontend + admin)
src/tasks/ — background tokio tasks (session cleanup)
src/theme.rs — light/dark theme with SSR cookie + WASM localStorage
src/webp.rs — zenwebp encode/decode (image crate has no WebP)
Tiptap Editor Subproject
Rich-text editor in libs/tiptap-editor/, built as an IIFE library exposing window.TiptapEditor.
- Output:
public/tiptap/ make buildrunsnpm install && npx vite buildinsidelibs/tiptap-editorsrc/pages/admin/write.rsinitializes viajs_sys::eval, pollswindow.__tiptap_ready
Do not edit public/tiptap/ — they are build artifacts.
Syntax Highlighting Pipeline
themes/contains Catppuccin Latte (light) and Mocha (dark).tmThemefilessyntaxes/has custom Sublime syntax definitions (Kotlin, Swift)src/bin/generate_highlight_css.rsgeneratespublic/highlight.csswith class-based rules scoped under.md-content pre code, with.darkprefix for dark modesrc/highlight.rsuses syntect at runtime for code block highlighting- All gated behind
#[cfg(feature = "server")]
Auth & Session
- Registration: first user becomes
admin; subsequent registrations rejected with"Registration is closed" - Login: sets an HttpOnly cookie via
FullstackContext::add_response_header - Session validation:
get_current_userreadssessioncookie, queriessessions+userstables - Background cleanup:
tasks::session_cleanup::run_cleanup()deletes expired sessions every hour
Caching
- Post/tag caches (
src/cache.rs): moka future-based, TTL varies by data type (60s–600s). Invalidated on writes. - Image processing cache (
src/api/image.rs): two-tier — in-memory moka cache + disk cache inuploads/.cache/. Keyed by path + query params.
Testing
cargo test # standard Rust test suite
dx check # Dioxus type-check (catches component/Router issues)
cargo clippy # lint
Most tests use #[cfg(all(test, feature = "server"))] — they only run when the server feature is active (which is the default). No integration tests requiring a database connection.
Image Processing Constraints
- The
imagecrate is configured without WebP support (default-features = false, features = ["jpeg", "png", "gif"]). Do not add WebP to the image crate features. - All WebP encode/decode goes through
zenwebpviasrc/webp.rs. - Upload pipeline auto-converts non-GIF/non-WebP images to WebP, keeping original format if WebP is larger.
- Image serving supports on-the-fly resize (
w,h), thumbnail (thumb=WxH), rotation (90/180/270), and format conversion.
Build Artifacts (gitignored)
public/style.css— Tailwind outputpublic/highlight.css— generated bygenerate_highlight_cssbinarypublic/tiptap/— Vite build output/dist,/.dioxus,/targetnode_modules(insidelibs/tiptap-editor/)uploads/.cache/— image processing disk cache
Notes
randis optional and only enabled by theserverfeature; it is not compiled into the WASM frontend.#[allow(unused_mut, unused_variables)]onWritecomponent is intentional —mutsignals are used in#[cfg(target_arch = "wasm32")]blocks stripped in server builds.- Server uses incremental rendering with 300s cache (
IncrementalRendererConfiginsrc/main.rs).