use std::net::SocketAddr; use clap::Parser; use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; use nx9_auth::{ api, cli::{self, Cli, Commands}, config::Config, db, db::repository::sessions as session_repo, state::AppState, }; #[tokio::main] async fn main() -> anyhow::Result<()> { // Parse CLI arguments first (before any logging so --help works cleanly) let cli = Cli::parse(); // Initialize logging based on the command and verbosity let is_serve = matches!(cli.command, Commands::Serve); if is_serve { // Structured JSON logging for production server deployment tracing_subscriber::registry() .with( EnvFilter::try_from_default_env() .unwrap_or_else(|_| "nx9_auth=info,tower_http=info".parse().unwrap()), ) .with(fmt::layer().json()) .init(); } else if cli.verbose { // Human-readable compact logging for verbosity in subcommands tracing_subscriber::registry() .with( EnvFilter::try_from_default_env() .unwrap_or_else(|_| "nx9_auth=debug".parse().unwrap()), ) .with(fmt::layer().compact()) .init(); } else { // Silence info/debug logging for clean operator CLI commands tracing_subscriber::registry() .with( EnvFilter::try_from_default_env() .unwrap_or_else(|_| "nx9_auth=warn".parse().unwrap()), ) .with(fmt::layer().compact()) .init(); } // Load configuration let config_opt = if matches!( cli.command, Commands::Init { .. } | Commands::ConfigPath { .. } ) { // For init/config-path commands, a missing override config is fine if let Some(ref path) = cli.config { if path.exists() { Some(Config::load(path)?) } else { let mut cfg = Config { config_path: Some(path.clone()), ..Default::default() }; cfg.resolve_paths(); Some(cfg) } } else { Config::find_and_load(None)? } } else { Config::find_and_load(cli.config.as_deref())? }; let config = match config_opt { Some(cfg) => cfg, None => { // init and config-path are allowed to run without an existing config file. // We use default Config structure for them. if matches!( cli.command, Commands::Init { .. } | Commands::ConfigPath { .. } ) { let mut cfg = Config::default(); cfg.resolve_paths(); cfg } else { eprintln!( "\nError: No configuration found.\n\nRun:\n\n nx9-auth init\n\nOr if running in Docker:\n\n docker exec -it nx9-auth nx9-auth init\n" ); std::process::exit(1); } } }; tracing::info!( version = env!("CARGO_PKG_VERSION"), git_commit = env!("GIT_COMMIT"), "nx9-auth starting" ); // Dispatch to serve or CLI command match cli.command { Commands::Serve => run_server(config).await, cmd => cli::run(cmd, config).await, } } /// Start the HTTP server (Milestone B+). async fn run_server(config: Config) -> anyhow::Result<()> { // Open DB pool and run migrations let pool = db::create_pool(&config.database.path).await?; db::run_migrations(&pool).await?; // Cleanup expired sessions at startup (one-shot, fire-and-forget) let pool_clone = pool.clone(); tokio::spawn(async move { match session_repo::cleanup_expired(&pool_clone).await { Ok(n) => tracing::info!(removed = n, "expired sessions cleaned up"), Err(e) => tracing::warn!(error = %e, "session cleanup failed"), } }); // Build application state let state = AppState::new(pool, config.clone()); // Build router let app = api::router::build(state); // Bind and serve let addr: SocketAddr = format!("{}:{}", config.server.host, config.server.port) .parse() .map_err(|e| anyhow::anyhow!("invalid bind address: {}", e))?; let listener = tokio::net::TcpListener::bind(addr).await?; tracing::info!( address = %addr, "server listening" ); println!( "\nServer listening on:\n\n http://{}\n\nHealth:\n\n http://{}/health\n", addr, addr ); axum::serve( listener, app.into_make_service_with_connect_info::(), ) .await?; Ok(()) }