use crate::{AppState, error::Result}; use axum::{Router, http::HeaderValue}; use std::{env, sync::Arc}; use tokio::net::TcpListener; use tower_http::{ cors::{Any, CorsLayer}, services::{ServeDir, ServeFile}, }; pub struct App { app_state: AppState, } impl App { pub fn new(app_state: AppState) -> Self { Self { app_state } } pub async fn serve(self) -> Result<()> { log::debug!("Starting API..."); // Build CORS layer. // // In production both the UI and API are served from the same origin so // CORS is a non-issue. In development, Vite proxies all /api/* calls so // the browser also never makes cross-origin requests directly to this // server. We keep a permissive default for convenience, but restrict it // when CORS_ORIGIN is explicitly set. let cors = match env::var("CORS_ORIGIN") { Ok(origin) if origin != "*" => { let header_val = origin .parse::() .expect("CORS_ORIGIN is not a valid header value"); CorsLayer::new() .allow_origin(header_val) .allow_methods(Any) .allow_headers(Any) .allow_credentials(true) } _ => CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any), }; // Serve the built React frontend from ui/dist (relative to the working // directory). Falls back gracefully if the directory does not exist yet // (e.g. during development when using `npm run dev`). let frontend_dir = env::current_dir() .unwrap_or_default() .join("ui") .join("dist"); // For SPA routing: any path not matched by a real file (e.g. /map/) // falls back to index.html so React can handle client-side routing. let index_html = frontend_dir.join("index.html"); let serve_dir = ServeDir::new(&frontend_dir).not_found_service(ServeFile::new(index_html)); let app = Router::new() .nest("/api", crate::get_routes()) .fallback_service(serve_dir) .layer(cors) .with_state(Arc::new(self.app_state)); let api_port: String = env::var("API_PORT").expect("Expected a port in the environment"); let addr = format!("0.0.0.0:{}", api_port); let listener = TcpListener::bind(&addr).await?; log::info!("API is listening on {}", &addr); Ok(axum::serve(listener, app).await?) } }