LCOV - code coverage report
Current view: top level - service/src - main.rs (source / functions) Coverage Total Hit
Test: Rust Backend Coverage Lines: 0.0 % 81 0
Test Date: 2025-12-20 21:58:40 Functions: 0.0 % 14 0

            Line data    Source code
       1              : #![deny(
       2              :     clippy::expect_used,
       3              :     clippy::panic,
       4              :     clippy::print_stdout,
       5              :     clippy::todo,
       6              :     clippy::unimplemented,
       7              :     clippy::unwrap_used
       8              : )]
       9              : 
      10              : use std::sync::Arc;
      11              : 
      12              : use async_graphql::{EmptySubscription, Schema};
      13              : use axum::{
      14              :     http::{header::HeaderValue, Method, StatusCode},
      15              :     middleware,
      16              :     response::IntoResponse,
      17              :     routing::get,
      18              :     Extension, Router,
      19              : };
      20              : use std::net::SocketAddr;
      21              : use tinycongress_api::{
      22              :     build_info::BuildInfoProvider,
      23              :     config::Config,
      24              :     db::setup_database,
      25              :     graphql::{graphql_handler, graphql_playground, MutationRoot, QueryRoot},
      26              :     http::{build_security_headers, security_headers_middleware},
      27              :     identity::{self, repo::PgAccountRepo},
      28              :     rest::{self, ApiDoc},
      29              : };
      30              : use tower_http::cors::{AllowOrigin, Any, CorsLayer};
      31              : use utoipa::OpenApi;
      32              : use utoipa_swagger_ui::SwaggerUi;
      33              : 
      34              : // Health check handler
      35            0 : async fn health_check() -> impl IntoResponse {
      36            0 :     StatusCode::OK
      37            0 : }
      38              : 
      39              : #[tokio::main]
      40            0 : async fn main() -> Result<(), anyhow::Error> {
      41              :     // Load and validate configuration first (fail-fast)
      42            0 :     let config = Config::load().map_err(|e| anyhow::anyhow!("{e}"))?;
      43              : 
      44              :     // Set up logging from config
      45            0 :     std::env::set_var("RUST_LOG", &config.logging.level);
      46            0 :     tracing_subscriber::fmt::init();
      47              : 
      48              :     // Init banner so container logs clearly show startup
      49            0 :     tracing::info!(
      50              :         version = env!("CARGO_PKG_VERSION"),
      51            0 :         "tinycongress-api starting up"
      52              :     );
      53              : 
      54              :     // Database connection
      55            0 :     tracing::info!("Connecting to database...");
      56            0 :     let pool = setup_database(&config.database).await?;
      57              : 
      58            0 :     let build_info = BuildInfoProvider::from_env();
      59            0 :     let build_info_snapshot = build_info.build_info();
      60            0 :     tracing::info!(
      61              :         version = %build_info_snapshot.version,
      62              :         git_sha = %build_info_snapshot.git_sha,
      63              :         build_time = %build_info_snapshot.build_time,
      64            0 :         "resolved build metadata"
      65              :     );
      66              : 
      67              :     // Create the GraphQL schema
      68            0 :     let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription)
      69            0 :         .data(pool.clone()) // Pass the database pool to the schema
      70            0 :         .data(build_info.clone())
      71            0 :         .finish();
      72              : 
      73              :     // Create repositories
      74            0 :     let account_repo: Arc<dyn identity::repo::AccountRepo> =
      75            0 :         Arc::new(PgAccountRepo::new(pool.clone()));
      76              : 
      77              :     // Build CORS layer from config
      78            0 :     let cors_origins = &config.cors.allowed_origins;
      79            0 :     let allow_origin: AllowOrigin = if cors_origins.iter().any(|o| o == "*") {
      80            0 :         tracing::warn!("CORS configured to allow any origin - not recommended for production");
      81            0 :         AllowOrigin::any()
      82            0 :     } else if cors_origins.is_empty() {
      83            0 :         tracing::info!(
      84            0 :             "CORS allowed origins not configured - cross-origin requests will be blocked"
      85              :         );
      86            0 :         AllowOrigin::list(Vec::<HeaderValue>::new())
      87              :     } else {
      88            0 :         let origins: Vec<HeaderValue> = cors_origins
      89            0 :             .iter()
      90            0 :             .filter_map(|origin| origin.parse().ok())
      91            0 :             .collect();
      92            0 :         tracing::info!(origins = ?cors_origins, "CORS allowed origins configured");
      93            0 :         AllowOrigin::list(origins)
      94              :     };
      95              : 
      96              :     // Build security headers layer if enabled
      97            0 :     let security_headers = if config.security_headers.enabled {
      98            0 :         tracing::info!("Security headers enabled");
      99            0 :         Some(build_security_headers(&config.security_headers))
     100              :     } else {
     101            0 :         tracing::info!("Security headers disabled");
     102            0 :         None
     103              :     };
     104              : 
     105              :     // REST API v1 routes
     106            0 :     let rest_v1 = Router::new().route("/build-info", get(rest::get_build_info));
     107              : 
     108              :     // Build the API
     109            0 :     let mut app = Router::new()
     110              :         // GraphQL endpoint - POST always enabled, GET (playground) is conditional
     111            0 :         .route("/graphql", {
     112            0 :             let route = axum::routing::post(graphql_handler);
     113            0 :             if config.graphql.playground_enabled {
     114            0 :                 tracing::info!("GraphQL Playground enabled at /graphql");
     115            0 :                 route.get(graphql_playground)
     116              :             } else {
     117            0 :                 tracing::info!(
     118            0 :                     "GraphQL Playground disabled (enable via TC_GRAPHQL__PLAYGROUND_ENABLED=true)"
     119              :                 );
     120            0 :                 route
     121              :             }
     122              :         })
     123              :         // REST API v1
     124            0 :         .nest("/api/v1", rest_v1)
     125              :         // Identity routes
     126            0 :         .merge(identity::http::router())
     127              :         // Health check route
     128            0 :         .route("/health", get(health_check))
     129              :         // Add the schema to the extension
     130            0 :         .layer(Extension(schema))
     131            0 :         .layer(Extension(pool.clone()))
     132            0 :         .layer(Extension(account_repo))
     133            0 :         .layer(Extension(build_info))
     134            0 :         .layer(
     135            0 :             CorsLayer::new()
     136            0 :                 .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
     137            0 :                 .allow_headers(Any)
     138            0 :                 .allow_origin(allow_origin),
     139              :         );
     140              : 
     141              :     // Add security headers middleware if enabled
     142            0 :     if let Some(headers) = security_headers {
     143            0 :         app = app
     144            0 :             .layer(middleware::from_fn(security_headers_middleware))
     145            0 :             .layer(Extension(headers));
     146            0 :     }
     147              : 
     148              :     // Add Swagger UI if enabled (disabled by default for security)
     149            0 :     if config.swagger.enabled {
     150            0 :         tracing::info!("Swagger UI enabled at /swagger-ui");
     151            0 :         app = app
     152            0 :             .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()));
     153              :     } else {
     154            0 :         tracing::info!("Swagger UI disabled (enable via TC_SWAGGER__ENABLED=true)");
     155              :     }
     156              : 
     157              :     // Start the server
     158            0 :     let addr = SocketAddr::from(([0, 0, 0, 0], config.server.port));
     159            0 :     tracing::info!(
     160            0 :         graphql = %format!("http://{}/graphql", addr),
     161            0 :         rest = %format!("http://{}/api/v1", addr),
     162            0 :         "Starting server"
     163              :     );
     164              : 
     165            0 :     let listener = tokio::net::TcpListener::bind(addr).await?;
     166            0 :     axum::serve(listener, app).await?;
     167              : 
     168            0 :     Ok(())
     169            0 : }
        

Generated by: LCOV version 2.0-1