Files
api_rust/src/main.rs

180 lines
8.8 KiB
Rust
Executable File

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::<url::Url>() {
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::<header::HeaderValue>().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();
}