87 lines
2.5 KiB
Rust
87 lines
2.5 KiB
Rust
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<CorsLayer> {
|
|
let parsed: Vec<HeaderValue> = origins
|
|
.iter()
|
|
.map(|o| o.parse::<HeaderValue>())
|
|
.collect::<Result<_, _>>()
|
|
.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]))
|
|
}
|