This commit is contained in:
2026-06-17 00:21:00 +02:00
commit 408e48c568
41 changed files with 6617 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
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]))
}