Skip to main content

ito_web/
server.rs

1//! HTTP server setup and configuration.
2
3use axum::{Router, middleware, routing::get};
4use std::net::SocketAddr;
5use std::path::PathBuf;
6use std::sync::Arc;
7use tower_http::cors::CorsLayer;
8
9use crate::api;
10use crate::auth::{self, AuthState};
11use crate::frontend;
12use crate::terminal::{self, TerminalState};
13
14/// Server configuration.
15#[derive(Debug, Clone)]
16pub struct ServeConfig {
17    /// Root directory to serve (typically the project root).
18    pub root: PathBuf,
19    /// Address to bind to.
20    pub bind: String,
21    /// Port to listen on.
22    pub port: u16,
23}
24
25impl Default for ServeConfig {
26    fn default() -> Self {
27        Self {
28            root: PathBuf::from("."),
29            bind: "127.0.0.1".to_string(),
30            port: 9009,
31        }
32    }
33}
34
35/// Start the web server.
36pub async fn serve(config: ServeConfig) -> miette::Result<()> {
37    let root = config.root.canonicalize().unwrap_or(config.root.clone());
38
39    // Generate token for non-loopback addresses
40    let token = if auth::is_loopback(&config.bind) {
41        None
42    } else {
43        Some(auth::generate_token(&root))
44    };
45
46    let auth_state = Arc::new(AuthState {
47        token: token.clone(),
48    });
49    let terminal_state = Arc::new(TerminalState { root: root.clone() });
50
51    let app = Router::new()
52        // Frontend routes
53        .route("/", get(frontend::index))
54        .route("/app.js", get(frontend::app_js))
55        // Terminal WebSocket
56        .route("/ws/terminal", get(terminal::ws_handler))
57        .with_state(terminal_state)
58        // API routes
59        .nest("/api", api::router(root.clone()))
60        // Auth middleware (checks token for non-loopback)
61        .layer(middleware::from_fn_with_state(
62            auth_state,
63            auth::auth_middleware,
64        ))
65        // CORS for development
66        .layer(CorsLayer::permissive());
67
68    let addr: SocketAddr = format!("{}:{}", config.bind, config.port)
69        .parse()
70        .map_err(|e| miette::miette!("Invalid address: {e}"))?;
71
72    let listener = tokio::net::TcpListener::bind(addr)
73        .await
74        .map_err(|e| miette::miette!("Failed to bind to {addr}: {e}"))?;
75
76    // Print URL with token if required
77    let url = if let Some(t) = &token {
78        format!("http://{addr}/?token={t}")
79    } else {
80        format!("http://{addr}/")
81    };
82    println!("Serving {} at {url}", root.display());
83
84    axum::serve(listener, app)
85        .await
86        .map_err(|e| miette::miette!("Server error: {e}"))?;
87
88    Ok(())
89}