use std::sync::Arc; use axum::extract::DefaultBodyLimit; use axum::http::{HeaderName, HeaderValue, Method}; use axum::Router; use sqlx::postgres::PgPoolOptions; use sqlx::PgPool; use tower_http::cors::{AllowOrigin, CorsLayer}; use tower_http::trace::TraceLayer; use crate::config::{AuthConfig, Config, UploadConfig}; use crate::storage::{LocalStorage, Storage}; #[derive(Clone)] pub struct AppState { pub db: PgPool, pub storage: Arc, pub auth: AuthConfig, pub upload: UploadConfig, } pub async fn build(config: Config) -> anyhow::Result { let db = PgPoolOptions::new() .max_connections(10) .connect(&config.database_url) .await?; sqlx::migrate!("./migrations").run(&db).await?; let storage: Arc = Arc::new(LocalStorage::new(config.storage_dir.clone())); let state = AppState { db, storage, auth: config.auth.clone(), upload: config.upload.clone(), }; Ok(router(state).layer(cors_layer(&config.cors_allowed_origins))) } /// Build a router from a pre-assembled state. Used by integration tests /// so they can swap in a test DB pool and a `tempfile`-backed storage. pub fn router(state: AppState) -> Router { let max_request_bytes = state.upload.max_request_bytes; Router::new() .nest("/api/v1", crate::api::routes()) .layer(DefaultBodyLimit::max(max_request_bytes)) .with_state(state) .layer(TraceLayer::new_for_http()) } fn cors_layer(allowed_origins: &[String]) -> CorsLayer { if allowed_origins.is_empty() { // Same-origin only — no CORS headers emitted. return CorsLayer::new(); } let origins: Vec = allowed_origins .iter() .filter_map(|o| HeaderValue::from_str(o).ok()) .collect(); CorsLayer::new() .allow_origin(AllowOrigin::list(origins)) .allow_credentials(true) .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]) .allow_headers([ HeaderName::from_static("content-type"), HeaderName::from_static("authorization"), ]) }