feat: implement SEO improvements, listing profiles, service images, and towns serviced

This commit is contained in:
2026-03-08 15:12:53 -04:00
parent 6c95a7d201
commit da22c4f19a
31 changed files with 1921 additions and 42 deletions

View File

@@ -4,11 +4,14 @@ use axum::{
};
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;
@@ -19,9 +22,14 @@ 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"
@@ -38,22 +46,47 @@ async fn main() {
)
.init();
tracing::info!("Starting NewEnglandBio API server...");
tracing::info!("🚀 Starting NewEnglandBio API server...");
// Load environment variables
dotenv::dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
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!(frontend_origin = %frontend_origin, "Configuration loaded");
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...");
let db_pool = PgPool::connect(&database_url)
.await
.expect("Failed to connect to database");
tracing::info!("Database connection established");
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);
@@ -81,6 +114,22 @@ async fn main() {
.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));
@@ -94,11 +143,27 @@ async fn main() {
.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("/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);