mod auth; mod config; mod db; mod error; mod mail; mod models; mod routes; mod state; use std::sync::Arc; use axum::http::{header, HeaderValue, Method}; use axum::Router; use time::Duration; use tower_http::cors::{AllowOrigin, CorsLayer}; use tower_http::trace::TraceLayer; use tower_sessions::{Expiry, SessionManagerLayer}; use tower_sessions_sqlx_store::PostgresStore; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use config::Config; use mail::Mailer; use state::AppState; #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::registry() .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info,sqlx=warn".into())) .with(tracing_subscriber::fmt::layer()) .init(); let config = Config::from_env()?; tracing::info!(port = config.port, "starting shoplist-backend"); let pool = db::connect(&config.database_url).await?; let mailer = Mailer::from_config(&config.smtp)?; // Session store (separate table set, managed by the store's own migrations). let session_store = PostgresStore::new(pool.clone()); session_store.migrate().await?; let session_layer = SessionManagerLayer::new(session_store) .with_secure(false) // set true behind HTTPS in production .with_same_site(tower_sessions::cookie::SameSite::Lax) .with_expiry(Expiry::OnInactivity(Duration::days(30))); let cors = build_cors(&config.cors_origins)?; let state = AppState { pool, config: Arc::new(config.clone()), mailer, }; let api = Router::new() .merge(routes::router()) .nest("/auth", auth::routes::router()); let app = Router::new() .nest("/api", api) .layer(cors) .layer(session_layer) .layer(TraceLayer::new_for_http()) .with_state(state); let addr = format!("{}:{}", config.host, config.port); let listener = tokio::net::TcpListener::bind(&addr).await?; tracing::info!("listening on {addr}"); axum::serve(listener, app).await?; Ok(()) } fn build_cors(origins: &[String]) -> anyhow::Result { let parsed: Vec = origins .iter() .map(|o| o.parse::()) .collect::>() .map_err(|e| anyhow::anyhow!("invalid CORS origin: {e}"))?; Ok(CorsLayer::new() .allow_origin(AllowOrigin::list(parsed)) .allow_credentials(true) .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE]) .allow_headers([header::CONTENT_TYPE])) }