aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorUMTS at Teleco <crt@teleco.ch>2025-12-13 02:48:13 +0100
committerUMTS at Teleco <crt@teleco.ch>2025-12-13 02:48:13 +0100
commite52b8e1c2e110d0feb74feb7905c2ff064b51d55 (patch)
tree3090814e422250e07e72cf1c83241ffd95cf20f7 /src/main.rs
committing to insanityHEADmaster
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs359
1 files changed, 359 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..17702f4
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,359 @@
+// 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<serde_json::Value> {
+ Json(json!({
+ "name": "SeckelAPI",
+ "version": VERSION
+ }))
+}
+
+// Health check handler with database connectivity test
+async fn health_check(State(state): State<AppState>) -> Json<serde_json::Value> {
+ // 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::<SocketAddr>(),
+ )
+ .await?;
+ Ok(())
+}
+
+async fn wait_for_database(config: &DatabaseConfig) -> Result<Database> {
+ 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,
+}