You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

141 lines
4.6 KiB
Rust

//! AI Weekly Synth — Rust/Axum backend entry point.
//!
//! Loads configuration, connects to Postgres, runs migrations,
//! and starts the HTTP server. Also supports the `create-admin` CLI subcommand.
mod cli;
mod logging;
use anyhow::Context;
use clap::Parser;
use sqlx::postgres::PgPoolOptions;
use tracing_subscriber::{fmt, EnvFilter};
use ai_synth_backend::app_state;
use ai_synth_backend::config::AppConfig;
use ai_synth_backend::db;
use ai_synth_backend::models::user::UserRole;
use ai_synth_backend::router;
use ai_synth_backend::services::scraper;
use crate::cli::{Cli, Commands};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Load .env file if present (not an error if missing)
dotenvy::dotenv().ok();
// Initialize tracing
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,ai_synth_backend=debug"));
fmt()
.with_env_filter(filter)
.event_format(logging::ErrorLocationFormat)
.init();
let cli = Cli::parse();
// Load and validate configuration
let config = AppConfig::from_env().map_err(|e| anyhow::anyhow!(e))?;
config.validate().map_err(|e| anyhow::anyhow!(e))?;
tracing::info!("Configuration loaded successfully");
// Create database connection pool
let pool = PgPoolOptions::new()
.max_connections(10)
.connect(&config.database_url)
.await
.context("Failed to connect to Postgres")?;
tracing::info!("Connected to Postgres");
// Run migrations
run_migrations(&pool).await?;
match cli.command.unwrap_or(Commands::Serve) {
Commands::Serve => {
let http_client = scraper::build_scraper_client()?;
let state = app_state::AppState::new(config.clone(), pool, http_client);
// Load provider rate limits from DB into in-memory limiter
if let Err(e) = state.provider_rate_limiter.reload_from_db(&state.pool).await {
tracing::warn!("Failed to load provider rate limits from DB: {:?}. Using defaults.", e);
}
let shutdown_pool = state.pool.clone();
let app = router::build_router(state, &config);
let addr = format!("0.0.0.0:{}", config.port);
tracing::info!("Starting server on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr)
.await
.context("Failed to bind to address")?;
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.context("Server error")?;
tracing::info!("Server shut down, closing database pool...");
shutdown_pool.close().await;
tracing::info!("Shutdown complete.");
}
Commands::CreateAdmin { email } => {
tracing::info!("Creating admin user: {}", email);
let existing = db::users::find_by_email(&pool, &email).await?;
match existing {
Some(user) => {
db::users::update_role(&pool, user.id, UserRole::Admin).await?;
tracing::info!("User {} promoted to admin.", email);
println!("User {} promoted to admin.", email);
}
None => {
db::users::create(&pool, &email, None, UserRole::Admin).await?;
tracing::info!("Admin user {} created.", email);
println!("Admin user {} created.", email);
}
}
}
}
Ok(())
}
/// Run database migrations at startup.
async fn run_migrations(pool: &sqlx::PgPool) -> anyhow::Result<()> {
tracing::info!("Running database migrations...");
sqlx::migrate!("./migrations")
.run(pool)
.await
.context("Failed to run database migrations")?;
tracing::info!("Migrations complete.");
Ok(())
}
/// Wait for SIGTERM or Ctrl+C, then return to trigger graceful shutdown.
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => tracing::info!("Received Ctrl+C, starting graceful shutdown..."),
_ = terminate => tracing::info!("Received SIGTERM, starting graceful shutdown..."),
}
}