yggdrasil/src/main.rs
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

80 lines
2.2 KiB
Rust

mod api;
mod cache;
mod auth;
mod components;
mod context;
mod db;
#[cfg(feature = "server")]
mod highlight;
mod hooks;
mod models;
mod pages;
mod router;
mod tasks;
mod theme;
mod utils;
mod webp;
fn main() {
#[cfg(feature = "server")]
{
dotenvy::dotenv().ok();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
if std::env::var("DATABASE_URL").is_err() {
tracing::error!("DATABASE_URL environment variable not set. Make sure .env exists or the variable is exported.");
eprintln!("ERROR: DATABASE_URL environment variable not set");
std::process::exit(1);
}
dioxus::server::serve(|| async move {
use dioxus::server::{axum, DioxusRouterExt, ServeConfig};
use tower_http::trace::TraceLayer;
tokio::spawn(async {
tasks::ip_purge::run_purge().await;
});
tokio::spawn(async {
tasks::session_cleanup::run_cleanup().await;
});
let config = ServeConfig::builder().incremental(
dioxus::server::IncrementalRendererConfig::default()
.invalidate_after(std::time::Duration::from_secs(300)),
);
let api_routes = axum::Router::new().route(
"/api/upload",
axum::routing::post(crate::api::upload::upload_image)
.layer(axum::extract::DefaultBodyLimit::disable()),
);
let static_routes = axum::Router::new().route(
"/uploads/{*path}",
axum::routing::get(crate::api::image::serve_image),
);
let dioxus_app =
axum::Router::new().serve_dioxus_application(config, router::AppRouter);
let router = api_routes
.merge(static_routes)
.merge(dioxus_app)
.layer(TraceLayer::new_for_http());
Ok(router)
});
}
#[cfg(not(feature = "server"))]
{
use router::AppRouter;
dioxus::launch(AppRouter);
}
}