Files
PostfixAdminRS/src/main.rs
T
2026-11-03 16:52:28 +01:00

141 lines
4.2 KiB
Rust

mod auth;
mod config;
mod db;
mod error;
mod hooks;
mod mailer;
mod models;
mod passwords;
mod routes;
mod sieve;
mod state;
mod ui;
mod validation;
use std::{net::SocketAddr, sync::Arc, time::Duration};
use axum::{
Router,
http::{HeaderName, HeaderValue, StatusCode},
routing::get,
};
use tower_http::{set_header::SetResponseHeaderLayer, timeout::TimeoutLayer, trace::TraceLayer};
use tracing_subscriber::EnvFilter;
use crate::{
auth::AuthStore,
config::AppConfig,
db::Db,
error::{AppError, AppResult},
state::AppState,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Err(error) = run().await {
eprintln!("postfixadmin-rs: {error}");
std::process::exit(1);
}
Ok(())
}
async fn run() -> AppResult<()> {
if std::env::args().any(|arg| arg == "--version" || arg == "-V") {
println!("postfixadmin-rs {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("postfixadmin_rs=info,tower_http=info")),
)
.init();
let config = Arc::new(AppConfig::load()?);
let db = Db::connect(&config).await?;
if config.database.dev_mock {
db.initialize_dev_mock(&config).await?;
}
db.ping().await?;
let warnings = db.check_schema(&config).await?;
for warning in &warnings {
tracing::warn!(%warning, "schema compatibility warning");
}
if std::env::args().any(|arg| arg == "--check-config") {
println!("configuration: ok");
println!("database: ok");
if config.database.dev_mock {
println!("database mode: development mock");
}
if config.database.read_only {
println!("database access: read-only");
}
for warning in warnings {
println!("warning: {warning}");
}
return Ok(());
}
let state = Arc::new(AppState {
auth: AuthStore::new(&config),
config: config.clone(),
db,
});
let api = routes::router(&state);
let inner = Router::<Arc<AppState>>::new()
.nest("/api", api)
.route("/assets/{*path}", get(ui::asset))
.fallback(ui::index)
.layer(TimeoutLayer::with_status_code(
StatusCode::REQUEST_TIMEOUT,
Duration::from_secs(60),
))
.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("content-security-policy"),
HeaderValue::from_static("default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'"),
))
.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("x-content-type-options"),
HeaderValue::from_static("nosniff"),
))
.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("x-frame-options"),
HeaderValue::from_static("DENY"),
))
.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("referrer-policy"),
HeaderValue::from_static("no-referrer"),
))
.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("permissions-policy"),
HeaderValue::from_static("camera=(), microphone=(), geolocation=()"),
))
.layer(TraceLayer::new_for_http());
let base = config.normalized_base_path();
let app: Router = if base == "/" {
inner.with_state(state.clone())
} else {
Router::<Arc<AppState>>::new()
.nest(&base, inner)
.with_state(state.clone())
};
let addr: SocketAddr = format!("{}:{}", config.host, config.port)
.parse()
.map_err(|error| AppError::config(format!("invalid listen address: {error}")))?;
tracing::info!(%addr, base_path = %base, "PostfixAdmin-RS listening");
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.map_err(|error| AppError::Io(std::io::Error::other(error)))?;
Ok(())
}