73 lines
2.3 KiB
Rust
73 lines
2.3 KiB
Rust
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::<HeaderValue>()
|
|
.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/<id>)
|
|
// 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?)
|
|
}
|
|
}
|