184 lines
6.3 KiB
Rust
184 lines
6.3 KiB
Rust
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<AtomicU64>,
|
|
count: Arc<AtomicU64>,
|
|
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<AppState> {
|
|
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<AppState> {
|
|
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),
|
|
)
|
|
}
|