//! Router assembly. Public API is versioned under `/v1` from day one; `/health` is //! unversioned for infrastructure probes. mod auth_routes; mod feed; mod health; mod ingest; mod interests; mod sources; mod tokens; use axum::Router; use axum::http::{Method, header}; use axum::routing::{delete, get, post}; use tower_http::compression::CompressionLayer; use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; use crate::state::AppState; /// Build the full application router. pub fn router(state: AppState) -> Router { let cors = build_cors(&state); let v1 = Router::new() // auth .route("/auth/register", post(auth_routes::register)) .route("/auth/login", post(auth_routes::login)) .route("/auth/logout", post(auth_routes::logout)) .route("/auth/me", get(auth_routes::me)) // feed .route("/feed", get(feed::get_feed)) .route("/feed/signals", post(feed::post_signal)) // sources .route("/sources", get(sources::list).post(sources::create)) .route("/sources/discover", post(sources::discover)) .route("/sources/import", post(sources::import)) .route("/sources/{id}", delete(sources::remove)) // interests .route("/interests", get(interests::list).put(interests::upsert)) .route("/interests/{id}", delete(interests::remove)) // api tokens .route("/tokens", get(tokens::list).post(tokens::create)) .route("/tokens/{id}", delete(tokens::revoke)) // ingest (bearer-token authenticated) .route("/ingest/candidates", post(ingest::submit)); Router::new() .route("/health", get(health::health)) .nest("/v1", v1) .layer(TraceLayer::new_for_http()) .layer(CompressionLayer::new()) .layer(cors) .with_state(state) } /// CORS is only needed in development, where the Vite dev server is a different origin /// from the API. In production nginx serves the SPA and proxies the API under one origin, /// so `cors_origins` is empty and no cross-origin headers are emitted. fn build_cors(state: &AppState) -> CorsLayer { if state.config.cors_origins.is_empty() { return CorsLayer::new(); } let origins = state .config .cors_origins .iter() .filter_map(|o| o.parse().ok()) .collect::>(); // Credentialed CORS forbids wildcard method/header lists, so enumerate them. CorsLayer::new() .allow_origin(origins) .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]) .allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION]) .allow_credentials(true) }