180 lines
8.8 KiB
Rust
Executable File
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();
|
|
}
|