use std::sync::{ Arc, atomic::{AtomicU64, Ordering}, }; use std::time::{SystemTime, UNIX_EPOCH}; use axum::{Router, http::StatusCode, middleware, response::IntoResponse, routing}; use tower_http::{services::ServeDir, trace::TraceLayer}; use crate::{handlers, state::AppState}; /// Simple global rate limiter: tracks request count per 60-second window. /// Not per-IP — suitable for a low-traffic personal app. #[derive(Clone)] struct RateLimiter { window: Arc, count: Arc, limit: u64, } impl RateLimiter { fn new(limit: u64) -> Self { Self { window: Arc::new(AtomicU64::new(0)), count: Arc::new(AtomicU64::new(0)), limit, } } fn check(&self) -> bool { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() / 60; let prev = self.window.load(Ordering::Relaxed); if now != prev { self.window.store(now, Ordering::Relaxed); self.count.store(1, Ordering::Relaxed); true } else { self.count.fetch_add(1, Ordering::Relaxed) + 1 <= self.limit } } } pub fn build_router(state: AppState) -> Router { Router::new() .merge(html_routes()) .merge(api_routes()) .nest_service("/static", ServeDir::new("static")) .layer(TraceLayer::new_for_http()) .with_state(state) } fn html_routes() -> Router { // Auth routes: 20 requests per minute globally. let limiter = RateLimiter::new(20); let auth = Router::new() .route( "/login", routing::get(handlers::html::get_login_page) .post(handlers::html::post_login), ) .route("/logout", routing::get(handlers::html::get_logout)) .route( "/register", routing::get(handlers::html::get_register_page) .post(handlers::html::post_register), ) .route_layer(middleware::from_fn(move |req: axum::extract::Request, next: middleware::Next| { let limiter = limiter.clone(); async move { if limiter.check() { next.run(req).await } else { StatusCode::TOO_MANY_REQUESTS.into_response() } } })); Router::new() .route("/", routing::get(handlers::html::get_activity_feed)) .route("/users", routing::get(handlers::html::get_users_list)) .route("/users/{id}", routing::get(handlers::html::get_user_profile)) .merge(auth) .route("/reviews/new", routing::get(handlers::html::get_new_review_page)) .route("/reviews", routing::post(handlers::html::post_review)) .route("/reviews/{id}/delete", routing::post(handlers::html::post_delete_review)) .route("/posters/{path}", routing::get(handlers::posters::get_poster)) .route("/feed.rss", routing::get(handlers::rss::get_feed)) .route("/users/{id}/feed.rss", routing::get(handlers::rss::get_user_feed)) } fn api_routes() -> Router { let limiter = RateLimiter::new(20); let auth_rate_limit = middleware::from_fn(move |req: axum::extract::Request, next: middleware::Next| { let limiter = limiter.clone(); async move { if limiter.check() { next.run(req).await } else { StatusCode::TOO_MANY_REQUESTS.into_response() } } }); Router::new().nest( "/api", Router::new() .route("/diary", routing::get(handlers::api::get_diary)) .route( "/movies/{id}/history", routing::get(handlers::api::get_review_history), ) .route("/reviews", routing::post(handlers::api::post_review)) .route("/reviews/{id}", routing::delete(handlers::api::delete_review)) .route( "/movies/{id}/sync-poster", routing::post(handlers::api::sync_poster), ) .route("/auth/login", routing::post(handlers::api::login)) .route("/auth/register", routing::post(handlers::api::register)) .route_layer(auth_rate_limit), ) }