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)
80 lines
2.2 KiB
Rust
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);
|
|
}
|
|
}
|