use axum::{ body::Body, extract::{Request, State}, http::{HeaderValue, Method, StatusCode, Uri}, response::IntoResponse, routing::any, Router, }; use std::net::SocketAddr; use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[derive(Clone)] struct Services { users_url: String, companies_url: String, job_seekers_url: String, customers_url: String, // ── 9 separate profession services ──────────────────────────────────── photographers_url: String, makeup_artists_url: String, tutors_url: String, developers_url: String, video_editors_url: String, graphic_designers_url: String, social_media_managers_url: String, fitness_trainers_url: String, catering_services_url: String, // ── Payments ───────────────────────────────────────────────────────── payments_url: String, client: reqwest::Client, } impl Services { fn from_env() -> Self { Self { users_url: std::env::var("USERS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8080".to_string()), companies_url: std::env::var("COMPANIES_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8081".to_string()), job_seekers_url: std::env::var("JOB_SEEKERS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8082".to_string()), customers_url: std::env::var("CUSTOMERS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8083".to_string()), photographers_url: std::env::var("PHOTOGRAPHERS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8085".to_string()), makeup_artists_url: std::env::var("MAKEUP_ARTISTS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8086".to_string()), tutors_url: std::env::var("TUTORS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8087".to_string()), developers_url: std::env::var("DEVELOPERS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8088".to_string()), video_editors_url: std::env::var("VIDEO_EDITORS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8089".to_string()), graphic_designers_url: std::env::var("GRAPHIC_DESIGNERS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8090".to_string()), social_media_managers_url: std::env::var("SOCIAL_MEDIA_MANAGERS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8091".to_string()), fitness_trainers_url: std::env::var("FITNESS_TRAINERS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8092".to_string()), catering_services_url: std::env::var("CATERING_SERVICES_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8093".to_string()), payments_url: std::env::var("PAYMENTS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:8094".to_string()), client: reqwest::Client::new(), } } fn resolve_upstream(&self, path: &str) -> Option { // Auth, users, roles, notifications, runtime-config, config, admin if path.starts_with("/api/auth") || path.starts_with("/api/me") || path.starts_with("/api/runtime-config") || path.starts_with("/api/config") || path.starts_with("/api/admin/roles") || path.starts_with("/api/admin/onboarding-config") || path.starts_with("/api/admin/dashboard-config") || path.starts_with("/api/admin/users") || path.starts_with("/api/admin/employees") || path.starts_with("/api/admin/departments") || path.starts_with("/api/admin/designations") || path.starts_with("/api/admin/approvals") || path.starts_with("/api/onboarding") { Some(self.users_url.clone()) } // Companies + Jobs + Applications + Packages else if path.starts_with("/api/companies") || path.starts_with("/api/jobs") || path.starts_with("/api/applications") || path.starts_with("/api/pricing") || path.starts_with("/api/admin/companies") || path.starts_with("/api/admin/jobs") { Some(self.companies_url.clone()) } // Job Seekers else if path.starts_with("/api/jobseeker") { Some(self.job_seekers_url.clone()) } // Customers + Requirements else if path.starts_with("/api/customers") || path.starts_with("/api/admin/customers") || path.starts_with("/api/admin/requirements") { Some(self.customers_url.clone()) } // ── 9 Separate Profession Services ──────────────────────────────── else if path.starts_with("/api/photographers") { Some(self.photographers_url.clone()) } else if path.starts_with("/api/makeup-artists") { Some(self.makeup_artists_url.clone()) } else if path.starts_with("/api/tutors") { Some(self.tutors_url.clone()) } else if path.starts_with("/api/developers") { Some(self.developers_url.clone()) } else if path.starts_with("/api/video-editors") { Some(self.video_editors_url.clone()) } else if path.starts_with("/api/graphic-designers") { Some(self.graphic_designers_url.clone()) } else if path.starts_with("/api/social-media-managers") { Some(self.social_media_managers_url.clone()) } else if path.starts_with("/api/fitness-trainers") { Some(self.fitness_trainers_url.clone()) } else if path.starts_with("/api/catering-services") { Some(self.catering_services_url.clone()) } // ── Payments + Invoices ─────────────────────────────────────────── else if path.starts_with("/api/payments") || path.starts_with("/api/admin/invoices") || path.starts_with("/api/admin/credits") || path.starts_with("/api/admin/revenue") || path.starts_with("/api/admin/pricing") { Some(self.payments_url.clone()) } // Wallet / Tracecoins (shared across profession services — route to a credits service) else if path.starts_with("/api/credits") { Some(self.payments_url.clone()) } else { None } } } fn build_cors() -> CorsLayer { let frontend_url = std::env::var("FRONTEND_URL") .unwrap_or_else(|_| "http://localhost:3000".to_string()); let admin_url = std::env::var("ADMIN_URL") .unwrap_or_else(|_| "http://localhost:3001".to_string()); let allowed_origins: Vec = vec![ frontend_url.parse().expect("Invalid FRONTEND_URL"), admin_url.parse().expect("Invalid ADMIN_URL"), ]; CorsLayer::new() .allow_origin(AllowOrigin::list(allowed_origins)) .allow_methods([ Method::GET, Method::POST, Method::PUT, Method::PATCH, Method::DELETE, Method::OPTIONS, ]) .allow_headers(AllowHeaders::mirror_request()) .allow_credentials(true) } #[tokio::main] async fn main() { tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), )) .with(tracing_subscriber::fmt::layer()) .init(); let services = Services::from_env(); let cors = build_cors(); let app = Router::new() .route("/api/{*path}", any(proxy_handler)) .route("/health", any(|| async { "Gateway OK" })) .layer(cors) .with_state(services); let port: u16 = std::env::var("PORT") .unwrap_or_else(|_| "8000".to_string()) .parse() .expect("PORT must be a valid u16"); let addr = SocketAddr::from(([0, 0, 0, 0], port)); tracing::info!("Gateway listening on {}", addr); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); } async fn proxy_handler( State(services): State, req: Request, ) -> impl IntoResponse { let path = req.uri().path().to_string(); let query = req.uri().query().map(|q| format!("?{}", q)).unwrap_or_default(); let Some(upstream_base) = services.resolve_upstream(&path) else { return (StatusCode::NOT_FOUND, "Route not found in gateway").into_response(); }; let target_url = format!("{}{}{}", upstream_base, path, query); let method = req.method().clone(); let headers = req.headers().clone(); let body = req.into_body(); match services .client .request(method, &target_url) .headers(headers) .body(reqwest::Body::wrap_stream(body.into_data_stream())) .send() .await { Ok(res) => { let status = StatusCode::from_u16(res.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); let res_headers = res.headers().clone(); let bytes = res.bytes().await.unwrap_or_default(); let res_body = Body::from(bytes); let mut response = res_body.into_response(); *response.status_mut() = status; for (name, value) in res_headers { if let Some(name) = name { response.headers_mut().insert(name, value); } } response } Err(e) => { tracing::error!("Gateway proxy error → {}: {}", target_url, e); (StatusCode::BAD_GATEWAY, format!("Gateway error: {}", e)).into_response() } } }