// Main entry point for the SeckelAPI server use anyhow::Result; use axum::{ http::Method, response::Json, routing::{get, post}, Router, }; use chrono::Utc; use serde_json::json; use std::net::SocketAddr; use std::sync::Arc; use tokio::time::{sleep, Duration}; use tower_governor::{ governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, GovernorLayer, }; use tower_http::cors::{Any, CorsLayer}; use tracing::{error, info, Level}; use tracing_subscriber; mod auth; mod config; mod db; mod logging; mod models; mod permissions; mod routes; mod scheduler; mod sql; use auth::SessionManager; use config::{Config, DatabaseConfig}; use logging::AuditLogger; use scheduler::QueryScheduler; use axum::extract::State; use db::{Database, DatabaseInitError}; use permissions::RBACManager; // Version pulled from Cargo.toml const VERSION: &str = env!("CARGO_PKG_VERSION"); // Root handler - simple version info async fn root() -> Json { Json(json!({ "name": "SeckelAPI", "version": VERSION })) } // Health check handler with database connectivity test async fn health_check(State(state): State) -> Json { // Test database connectivity let db_status = match sqlx::query("SELECT 1") .fetch_one(state.database.pool()) .await { Ok(_) => "connected", Err(_) => "disconnected", }; Json(json!({ "status": "running", "message": format!("SeckelAPI v{}: The schizophrenic database application JSON API", VERSION), "database": db_status })) } #[tokio::main] async fn main() -> Result<()> { // Initialize tracing tracing_subscriber::fmt().with_max_level(Level::INFO).init(); info!("Starting SeckelAPI Server..."); // Load configuration let config = Config::load()?; let config_arc = Arc::new(config.clone()); info!("- Configuration might have loaded successfully"); // Initialize database connection let database = wait_for_database(&config.database).await?; info!("- Database said 'Hello there!'"); // Initialize session manager let session_manager = SessionManager::new(config_arc.clone()); info!("- Session manager didn't crash on startup (yay!)"); // Spawn background task for session cleanup let cleanup_manager = session_manager.clone(); let cleanup_interval = config.security.session_cleanup_interval_minutes; tokio::spawn(async move { let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(cleanup_interval * 60)); loop { interval.tick().await; cleanup_manager.cleanup_expired_sessions(); info!("- Ran session cleanup task"); } }); info!( "- Background session cleanup task spawned (interval: {}min)", cleanup_interval ); // Initialize RBAC manager let rbac = RBACManager::new(&config); info!("- Overcomplicated RBAC manager has initialized"); // Initialize audit logger let logging = AuditLogger::new( config.logging.request_log.clone(), config.logging.query_log.clone(), config.logging.error_log.clone(), config.logging.warning_log.clone(), config.logging.info_log.clone(), config.logging.combined_log.clone(), config.logging.mask_passwords, config.logging.sensitive_fields.clone(), config.logging.custom_filters.clone(), )?; info!("- CIA Surveillance Service Agents have been initialized on your system (just kidding, just the logging stack)"); if !config.logging.custom_filters.is_empty() { let enabled_count = config .logging .custom_filters .iter() .filter(|f| f.enabled) .count(); info!("- {} custom log filter(s) active", enabled_count); } spawn_database_monitor(database.clone(), config.database.clone(), logging.clone()); // Initialize and spawn scheduled query tasks let scheduler = QueryScheduler::new(config_arc.clone(), database.clone(), logging.clone()); scheduler.spawn_tasks(); // Create CORS layer let cors = CorsLayer::new() .allow_methods([Method::GET, Method::POST]) .allow_headers(Any) .allow_origin(Any); // Build auth routes with rate limiting let mut auth_routes = Router::new() .route("/auth/login", post(routes::auth::login)) .route("/auth/logout", post(routes::auth::logout)) .route("/auth/status", get(routes::auth::status)); if config.security.enable_rate_limiting { info!( "- Auth rate limiting set to: {}/min, {}/sec per IP", config.security.auth_rate_limit_per_minute, config.security.auth_rate_limit_per_second ); let auth_governor_conf = Arc::new( GovernorConfigBuilder::default() .per_second(config.security.auth_rate_limit_per_second.max(1) as u64) .burst_size(config.security.auth_rate_limit_per_minute.max(1)) .key_extractor(SmartIpKeyExtractor) .finish() .expect("Failed to build auth rate limiter config ... you sure its configured?"), ); auth_routes = auth_routes.layer(GovernorLayer::new(auth_governor_conf)); } else { info!("- Auth rate limiting DISABLED (dont run in production like this plz) "); } // Build API routes with rate limiting let mut api_routes = Router::new() .route("/query", post(routes::query::execute_query)) .route("/permissions", get(routes::query::get_permissions)) .route( "/preferences", post(routes::preferences::handle_preferences), ); if config.security.enable_rate_limiting { info!( "- API rate limiting set to: {}/min, {}/sec per IP", config.security.api_rate_limit_per_minute, config.security.api_rate_limit_per_second ); let api_governor_conf = Arc::new( GovernorConfigBuilder::default() .per_second(config.security.api_rate_limit_per_second.max(1) as u64) .burst_size(config.security.api_rate_limit_per_minute.max(1)) .key_extractor(SmartIpKeyExtractor) .finish() .expect( "Failed to build API rate limiter config, ugh ... you sure its configured?", ), ); api_routes = api_routes.layer(GovernorLayer::new(api_governor_conf)); } // das so routes was es chan let app = Router::new() .route("/", get(root)) .route("/health", get(health_check)) .merge(auth_routes) .merge(api_routes) .layer(tower_http::limit::RequestBodyLimitLayer::new( config.server.request_body_limit_mb * 1024 * 1024, )) .layer(cors) .with_state(AppState { config: config.clone(), database, session_manager, rbac, logging, }); let addr = SocketAddr::from(([0, 0, 0, 0], config.server.port)); info!( "- SeckelAPI somehow started and should now be listening on {} :)", addr ); let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve( listener, app.into_make_service_with_connect_info::(), ) .await?; Ok(()) } async fn wait_for_database(config: &DatabaseConfig) -> Result { let retry_delay = config.connection_timeout_wait.max(1); loop { match Database::new(config).await { Ok(db) => return Ok(db), Err(DatabaseInitError::Retryable(err)) => { error!( "Database unavailable (retrying in {}s): {}", retry_delay, err ); sleep(Duration::from_secs(retry_delay)).await; } Err(DatabaseInitError::Fatal(err)) => { error!("Fatal database configuration error: {}", err); return Err(err); } } } } fn spawn_database_monitor(database: Database, db_config: DatabaseConfig, logging: AuditLogger) { let heartbeat_interval = db_config.connection_check.max(5); let retry_delay = db_config.connection_timeout_wait.max(1); tokio::spawn(async move { let mut ticker = tokio::time::interval(Duration::from_secs(heartbeat_interval)); loop { ticker.tick().await; match sqlx::query("SELECT 1").fetch_one(database.pool()).await { Ok(_) => { if !database.is_available() { info!("Database connectivity restored"); log_database_event( &logging, "database_reconnect", "Database connectivity restored", false, ) .await; } database.mark_available(); } Err(err) => { database.mark_unavailable(); error!( "Database heartbeat failed (retrying every {}s): {}", retry_delay, err ); log_database_event( &logging, "database_heartbeat_failure", &format!("Heartbeat failed: {}", err), true, ) .await; loop { sleep(Duration::from_secs(retry_delay)).await; match sqlx::query("SELECT 1").fetch_one(database.pool()).await { Ok(_) => { database.mark_available(); info!("Database reconnected successfully"); log_database_event( &logging, "database_reconnect", "Database reconnected successfully", false, ) .await; break; } Err(retry_err) => { error!( "Database still unavailable (retrying in {}s): {}", retry_delay, retry_err ); log_database_event( &logging, "database_retry_failure", &format!("Database retry failed: {}", retry_err), true, ) .await; } } } } } } }); } async fn log_database_event(logging: &AuditLogger, context: &str, message: &str, as_error: bool) { let request_id = AuditLogger::generate_request_id(); let timestamp = Utc::now(); let result = if as_error { logging .log_error( &request_id, timestamp, message, Some(context), Some("system"), None, ) .await } else { logging .log_info( &request_id, timestamp, message, Some(context), Some("system"), None, ) .await }; if let Err(err) = result { error!("Failed to record database event ({}): {}", context, err); } } #[derive(Clone)] pub struct AppState { pub config: Config, pub database: Database, pub session_manager: SessionManager, pub rbac: RBACManager, pub logging: AuditLogger, }