Skip to main content

opendev_web/
server.rs

1//! Axum application builder and server startup.
2
3use std::net::SocketAddr;
4use std::path::Path;
5
6use axum::Router;
7use axum::http::header;
8use tower_http::cors::CorsLayer;
9use tower_http::services::ServeDir;
10use tracing::info;
11
12use crate::routes;
13use crate::state::AppState;
14use crate::websocket::ws_handler;
15
16/// Build the Axum application router.
17pub fn build_app(state: AppState, static_dir: Option<&Path>) -> Router {
18    let cors = CorsLayer::new()
19        .allow_origin([
20            "http://localhost:5173".parse().unwrap(),
21            "http://localhost:3000".parse().unwrap(),
22        ])
23        .allow_methods([
24            axum::http::Method::GET,
25            axum::http::Method::POST,
26            axum::http::Method::PUT,
27            axum::http::Method::DELETE,
28            axum::http::Method::OPTIONS,
29        ])
30        .allow_headers([
31            header::CONTENT_TYPE,
32            header::AUTHORIZATION,
33            header::ACCEPT,
34            header::COOKIE,
35        ])
36        .allow_credentials(true);
37
38    let mut app = Router::new()
39        // API routes
40        .merge(routes::auth::router())
41        .merge(routes::config::router())
42        .merge(routes::sessions::router())
43        .merge(routes::chat::router())
44        .merge(routes::mcp::router())
45        .merge(routes::commands::router())
46        // Health check
47        .route("/api/health", axum::routing::get(health_check))
48        // WebSocket
49        .route("/ws", axum::routing::get(ws_handler))
50        .layer(cors)
51        .with_state(state);
52
53    // Serve static files if the directory exists.
54    if let Some(dir) = static_dir
55        && dir.exists()
56    {
57        let assets_dir = dir.join("assets");
58        if assets_dir.exists() {
59            app = app.nest_service("/assets", ServeDir::new(assets_dir));
60        }
61        // SPA fallback: serve index.html for all unmatched paths.
62        app = app.fallback_service(ServeDir::new(dir));
63    }
64
65    app
66}
67
68/// Health check endpoint.
69async fn health_check() -> axum::Json<serde_json::Value> {
70    axum::Json(serde_json::json!({
71        "status": "ok",
72        "service": "opendev-web-ui",
73    }))
74}
75
76/// Start the web server.
77pub async fn start_server(
78    state: AppState,
79    host: &str,
80    port: u16,
81    static_dir: Option<&Path>,
82) -> std::io::Result<()> {
83    let app = build_app(state, static_dir);
84
85    let addr: SocketAddr = format!("{host}:{port}")
86        .parse()
87        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
88
89    info!("Starting web server on {}", addr);
90
91    let listener = tokio::net::TcpListener::bind(addr).await?;
92    axum::serve(listener, app)
93        .await
94        .map_err(std::io::Error::other)
95}
96
97#[cfg(test)]
98#[path = "server_tests.rs"]
99mod tests;