298 lines
13 KiB
Rust
298 lines
13 KiB
Rust
use axum::{
|
|
body::Body,
|
|
extract::{Request, State},
|
|
http::{HeaderValue, Method, StatusCode},
|
|
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,
|
|
ugc_content_creators_url: String,
|
|
// ── Payments ─────────────────────────────────────────────────────────
|
|
payments_url: String,
|
|
// ── Employees (Internal) ─────────────────────────────────────────────
|
|
employees_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()),
|
|
ugc_content_creators_url: std::env::var("UGC_CONTENT_CREATORS_SERVICE_URL")
|
|
.unwrap_or_else(|_| "http://localhost:8095".to_string()),
|
|
payments_url: std::env::var("PAYMENTS_SERVICE_URL")
|
|
.unwrap_or_else(|_| "http://localhost:8094".to_string()),
|
|
employees_url: std::env::var("EMPLOYEES_SERVICE_URL")
|
|
.unwrap_or_else(|_| "http://localhost:8096".to_string()),
|
|
client: reqwest::Client::new(),
|
|
}
|
|
}
|
|
|
|
|
|
fn resolve_upstream(&self, path: &str) -> Option<String> {
|
|
// Auth, users, roles, notifications, runtime-config, config, KB, support
|
|
if path.starts_with("/api/auth")
|
|
|| path.starts_with("/api/users")
|
|
|| path.starts_with("/api/me")
|
|
|| path.starts_with("/api/profile")
|
|
|| path.starts_with("/api/onboarding")
|
|
|| path.starts_with("/api/roles")
|
|
|| path.starts_with("/api/notifications")
|
|
|| path.starts_with("/api/config")
|
|
|| path.starts_with("/api/kb")
|
|
|| path.starts_with("/api/packages")
|
|
|| path.starts_with("/api/support")
|
|
|| path.starts_with("/api/admin/roles")
|
|
|| path.starts_with("/api/admin/users")
|
|
|| path.starts_with("/api/admin/verifications")
|
|
|| path.starts_with("/api/admin/approvals")
|
|
|| path.starts_with("/api/admin/approval-cases")
|
|
|| path.starts_with("/api/admin/external-roles")
|
|
|| path.starts_with("/api/admin/dashboard-config")
|
|
|| path.starts_with("/api/admin/onboarding-config")
|
|
|| path.starts_with("/api/admin/dashboard")
|
|
|| path.starts_with("/api/admin/kb")
|
|
|| path.starts_with("/api/admin/support-cases")
|
|
|| path.starts_with("/api/admin/reviews")
|
|
|| path.starts_with("/api/admin/coupons")
|
|
|| path.starts_with("/api/admin/discounts")
|
|
|| path.starts_with("/api/admin/tracecoin-packages")
|
|
|| path.starts_with("/api/admin/reports")
|
|
|| path.starts_with("/api/admin/activity-logs")
|
|
{
|
|
Some(self.users_url.clone())
|
|
}
|
|
// ── Employees / Internal Admin (NEW) ──────────────────────────────
|
|
else if path.starts_with("/api/admin/auth")
|
|
|| path.starts_with("/api/admin/employees")
|
|
|| path.starts_with("/api/admin/departments")
|
|
|| path.starts_with("/api/admin/designations")
|
|
{
|
|
Some(self.employees_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")
|
|
|| path.starts_with("/api/admin/applications")
|
|
{
|
|
Some(self.companies_url.clone())
|
|
}
|
|
// Job Seekers
|
|
else if path.starts_with("/api/jobseeker") {
|
|
Some(self.job_seekers_url.clone())
|
|
}
|
|
// Customers + Leads
|
|
else if path.starts_with("/api/customers")
|
|
|| path.starts_with("/api/admin/customers")
|
|
|| path.starts_with("/api/admin/leads")
|
|
{
|
|
Some(self.customers_url.clone())
|
|
}
|
|
// ── 9 Separate Profession Services ────────────────────────────────
|
|
else if path.starts_with("/api/photographers") || path.starts_with("/api/admin/photographers") {
|
|
Some(self.photographers_url.clone())
|
|
}
|
|
else if path.starts_with("/api/makeup-artists") || path.starts_with("/api/admin/makeup-artists") {
|
|
Some(self.makeup_artists_url.clone())
|
|
}
|
|
else if path.starts_with("/api/tutors") || path.starts_with("/api/admin/tutors") {
|
|
Some(self.tutors_url.clone())
|
|
}
|
|
else if path.starts_with("/api/developers") || path.starts_with("/api/admin/developers") {
|
|
Some(self.developers_url.clone())
|
|
}
|
|
else if path.starts_with("/api/video-editors") || path.starts_with("/api/admin/video-editors") {
|
|
Some(self.video_editors_url.clone())
|
|
}
|
|
else if path.starts_with("/api/graphic-designers") || path.starts_with("/api/admin/graphic-designers") {
|
|
Some(self.graphic_designers_url.clone())
|
|
}
|
|
else if path.starts_with("/api/social-media-managers") || path.starts_with("/api/admin/social-media-managers") {
|
|
Some(self.social_media_managers_url.clone())
|
|
}
|
|
else if path.starts_with("/api/fitness-trainers") || path.starts_with("/api/admin/fitness-trainers") {
|
|
Some(self.fitness_trainers_url.clone())
|
|
}
|
|
else if path.starts_with("/api/catering-services") || path.starts_with("/api/admin/catering-services") {
|
|
Some(self.catering_services_url.clone())
|
|
}
|
|
else if path.starts_with("/api/ugc-content-creators") {
|
|
Some(self.ugc_content_creators_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())
|
|
}
|
|
// Admin runtime config management defaults to users service
|
|
else if path.starts_with("/api/admin/runtime-configs") {
|
|
Some(self.users_url.clone())
|
|
}
|
|
// Catch-all for any other admin endpoints → users service
|
|
else if path.starts_with("/api/admin/") {
|
|
Some(self.users_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<HeaderValue> = 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<Services>,
|
|
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()
|
|
}
|
|
}
|
|
}
|