use axum::{ http::{header, Method}, Router, }; use std::env; use tower_http::cors::CorsLayer; use tower_http::services::ServeDir; use crate::auth::structs::AppState; use crate::auth::auth::{auth_middleware, login, register, logout}; use crate::data::data::get_user; use crate::state::data::{get_counties_by_state, get_county_by_id}; use crate::listing::data::{get_listings, get_listing_by_id, get_listings_by_county, create_listing, update_listing, delete_listing}; use crate::service_listing::data::{get_service_listings, get_service_listing_by_id, get_service_listings_by_county, create_service_listing, update_service_listing, delete_service_listing}; use crate::upload::data::{upload_listing_image, delete_listing_image, upload_listing_banner, delete_listing_banner, upload_service_listing_image, delete_service_listing_image, upload_service_listing_banner, delete_service_listing_banner}; use crate::oil_prices::data::get_oil_prices_by_county; use axum::middleware; use sqlx::PgPool; use std::sync::Arc; mod auth; mod data; mod state; mod company; mod listing; mod service_listing; mod upload; mod oil_prices; mod stats; mod admin; mod subscription; mod banner; use crate::data::api_directory::get_api_directory; async fn health_check() -> &'static str { "NewEnglandBio API is running" } #[tokio::main] async fn main() { // Initialize tracing first (before any logging) // RUST_LOG env var controls log level, e.g. RUST_LOG=debug or RUST_LOG=api_rust=debug tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::from_default_env() .add_directive("api_rust=info".parse().unwrap()) ) .init(); tracing::info!("🚀 Starting NewEnglandBio API server..."); // Load environment variables dotenv::dotenv().ok(); let mut database_url = env::var("DATABASE_URL") .expect("💀 DATABASE_URL must be set") .trim() .trim_matches(|c| c == '"' || c == '\'') .to_string(); if !database_url.contains("://") { database_url = format!("postgres://{}", database_url); tracing::warn!("Added missing 'postgres://' scheme to DATABASE_URL"); } let frontend_origin = env::var("FRONTEND_ORIGIN").unwrap_or_else(|_| "http://localhost:9551".to_string()); let environment_mode = env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string()); tracing::info!("🌍 Environment Mode: {}", environment_mode); tracing::info!("🔗 Frontend Origin: {}", frontend_origin); // Parse database URL to show details without password let db_details = if let Ok(parsed) = database_url.parse::() { let host = parsed.host_str().unwrap_or("unknown_host"); let db_name = parsed.path().trim_start_matches('/'); format!("Host: {}, Database: {}", host, db_name) } else { "Unknown DB details".to_string() }; // Connect to PostgreSQL tracing::info!("⏳ Connecting to PostgreSQL database... ({})", db_details); let db_pool = match PgPool::connect(&database_url).await { Ok(pool) => { tracing::info!("✅ Database connection established successfully"); pool } Err(e) => { tracing::error!("💀 Failed to connect to database: {}", e); panic!("Database connection failed"); } }; let db = Arc::new(db_pool); // Create app state let state = AppState { db: db.clone(), jwt_secret: env::var("JWT_SECRET").expect("JWT_SECRET must be set"), }; // Configure CORS with credentials support for cookie auth let cors = CorsLayer::new() .allow_origin(tower_http::cors::AllowOrigin::exact(frontend_origin.parse::().unwrap())) .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]) .allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION, header::ACCEPT]) .allow_credentials(true); tracing::debug!("CORS configured for origin: {} with credentials", frontend_origin); // Build router with separated public and protected routes let protected_routes = Router::new() .route("/user", axum::routing::get(get_user)) .route("/company", axum::routing::any(crate::company::company::company_handler)) .route("/listing", axum::routing::get(get_listings)) .route("/listing", axum::routing::post(create_listing)) .route("/listing/:listing_id", axum::routing::get(get_listing_by_id)) .route("/listing/:listing_id", axum::routing::put(update_listing)) .route("/listing/:listing_id", axum::routing::delete(delete_listing)) .route("/service-listing", axum::routing::get(get_service_listings)) .route("/service-listing", axum::routing::post(create_service_listing)) .route("/service-listing/:listing_id", axum::routing::get(get_service_listing_by_id)) .route("/service-listing/:listing_id", axum::routing::put(update_service_listing)) .route("/service-listing/:listing_id", axum::routing::delete(delete_service_listing)) .route("/upload/listing/:listing_id/image", axum::routing::post(upload_listing_image)) .route("/upload/listing/:listing_id/image", axum::routing::delete(delete_listing_image)) .route("/upload/service-listing/:listing_id/image", axum::routing::post(upload_service_listing_image)) .route("/upload/service-listing/:listing_id/image", axum::routing::delete(delete_service_listing_image)) .route("/upload/listing/:listing_id/banner", axum::routing::post(upload_listing_banner)) .route("/upload/listing/:listing_id/banner", axum::routing::delete(delete_listing_banner)) .route("/upload/service-listing/:listing_id/banner", axum::routing::post(upload_service_listing_banner)) .route("/upload/service-listing/:listing_id/banner", axum::routing::delete(delete_service_listing_banner)) .route("/subscription", axum::routing::get(crate::subscription::data::get_subscription)) .route("/admin/banner", axum::routing::post(crate::banner::data::create_banner)) .route("/admin/banner/:banner_id", axum::routing::delete(crate::banner::data::delete_banner)) .merge(crate::admin::admin_routes()) .route_layer(middleware::from_fn_with_state(state.clone(), auth_middleware)); let public_routes = Router::new() .route("/", axum::routing::get(health_check)) .route("/health", axum::routing::get(health_check)) .route("/auth/register", axum::routing::post(register)) .route("/auth/login", axum::routing::post(login)) .route("/auth/logout", axum::routing::post(logout)) .route("/categories", axum::routing::get(crate::company::category::get_all_categories)) .route("/state/:state_abbr", axum::routing::get(get_counties_by_state)) .route("/state/:state_abbr/:county_id", axum::routing::get(get_county_by_id)) .route("/listings/county/:county_id", axum::routing::get(get_listings_by_county)) .route("/listings/:listing_id", axum::routing::get(crate::listing::data::get_listing_public)) .route("/service-listings/county/:county_id", axum::routing::get(get_service_listings_by_county)) .route("/service-listings/:listing_id", axum::routing::get(crate::service_listing::data::get_service_listing_public)) .route("/oil-prices/county/:county_id", axum::routing::get(get_oil_prices_by_county)) .route("/stats", axum::routing::get(crate::stats::data::get_latest_stats)) .route("/company/:company_id", axum::routing::get(crate::company::company::get_company_profile)) .route("/company/user/:user_id", axum::routing::get(crate::company::company::get_company_profile_by_user)) .route("/banner", axum::routing::get(crate::banner::data::get_active_banner)) .route("/api-directory", axum::routing::get(get_api_directory)) .route("/listings/sitemap/all", axum::routing::get(crate::listing::sitemap::get_all_active_listing_ids)) .route("/service-listings/sitemap/all", axum::routing::get(crate::service_listing::sitemap::get_all_active_service_listing_ids)); // Ensure upload directories exist tokio::fs::create_dir_all("/uploads/listings").await.ok(); tokio::fs::create_dir_all("/uploads/service").await.ok(); tokio::fs::create_dir_all("/uploads/banners").await.ok(); tokio::fs::create_dir_all("/uploads/service_banners").await.ok(); let app = public_routes .merge(protected_routes) .nest_service("/uploads", ServeDir::new("/uploads")) .with_state(state) .layer(cors); tracing::info!("Routes configured"); tracing::info!("Server is running on http://0.0.0.0:9552"); tracing::info!("Press Ctrl+C to stop"); // Run server axum::Server::bind(&"0.0.0.0:9552".parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); }