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}; /// Build an ActivityPub router from the service, excluding routes that /// conflict with HTML routes (/users/{id} and /users/{id}/following). /// Those AP endpoints are still served via the federation middleware layer /// applied to the whole AP router scope; the conflicting paths will need /// content-negotiation wrappers added in Phase 5. fn ap_routes(state: &AppState) -> Router { let config = state.ap_service.federation_config(); Router::new() .route("/.well-known/webfinger", routing::get(activitypub::webfinger::webfinger_handler)) .route("/users/{user_id}/inbox", routing::post(activitypub::inbox::inbox_handler)) .route("/users/{user_id}/outbox", routing::get(activitypub::outbox::outbox_handler)) .route("/users/{user_id}/followers", routing::get(activitypub::followers_handler::followers_handler)) .layer(config.middleware()) } /// 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::Acquire); if now != prev { // compare_exchange ensures only one thread wins the window reset if self.window.compare_exchange(prev, now, Ordering::AcqRel, Ordering::Relaxed).is_ok() { self.count.store(1, Ordering::Release); return true; } } self.count.fetch_add(1, Ordering::Relaxed) + 1 <= self.limit } } pub fn build_router(state: AppState) -> Router { let rate_limit = state.app_ctx.config.rate_limit; let ap_router = ap_routes(&state); Router::new() .merge(html_routes(rate_limit)) .merge(api_routes(rate_limit)) .nest_service("/static", ServeDir::new("static")) .layer(TraceLayer::new_for_http()) .with_state(state) .merge(ap_router) } fn html_routes(rate_limit: u64) -> Router { let limiter = RateLimiter::new(rate_limit); 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), ) .route( "/users/{id}/follow", routing::post(handlers::html::follow_remote_user), ) .route( "/users/{id}/unfollow", routing::post(handlers::html::unfollow_remote_user), ) .route( "/users/{id}/followers/accept", routing::post(handlers::html::accept_follower), ) .route( "/users/{id}/followers/reject", routing::post(handlers::html::reject_follower), ) .route( "/users/{id}/following-list", routing::get(handlers::html::get_following_page), ) .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(rate_limit: u64) -> Router { let limiter = RateLimiter::new(rate_limit); 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), ) }