Skip to main content

ito_backend/
server.rs

1//! HTTP server bootstrap and route assembly.
2//!
3//! [`serve`] is the single entry point: it wires up the v1 API router,
4//! authentication middleware, and CORS, then binds to the configured address.
5//! All business logic is in `ito-core`; this module handles transport concerns.
6
7use axum::{Router, middleware};
8use std::net::SocketAddr;
9use std::path::PathBuf;
10use std::sync::Arc;
11use tower_http::cors::CorsLayer;
12
13use crate::api;
14use crate::auth::{self, AuthState};
15use crate::state::AppState;
16
17/// Configuration for starting the backend server.
18#[derive(Debug, Clone)]
19pub struct BackendConfig {
20    /// Project root directory (parent of `.ito/`).
21    pub project_root: PathBuf,
22    /// Path to the `.ito/` directory. Defaults to `project_root.join(".ito")`.
23    pub ito_path: Option<PathBuf>,
24    /// Address to bind to (e.g., `"127.0.0.1"`).
25    pub bind: String,
26    /// Port to listen on.
27    pub port: u16,
28    /// Authentication token. If `None`, a deterministic token is generated.
29    pub token: Option<String>,
30    /// CORS allowed origins. `None` means permissive.
31    pub cors_origins: Option<Vec<String>>,
32}
33
34impl Default for BackendConfig {
35    fn default() -> Self {
36        Self {
37            project_root: PathBuf::from("."),
38            ito_path: None,
39            bind: "127.0.0.1".to_string(),
40            port: 9010,
41            token: None,
42            cors_origins: None,
43        }
44    }
45}
46
47/// Start the backend API server and block until it shuts down.
48///
49/// Assembles routes, auth middleware, and CORS, then binds to the configured
50/// address. Prints the listening address and token to stderr on startup.
51pub async fn serve(config: BackendConfig) -> miette::Result<()> {
52    let root = config
53        .project_root
54        .canonicalize()
55        .unwrap_or(config.project_root.clone());
56
57    // Resolve authentication token
58    let token = config.token.unwrap_or_else(|| auth::generate_token(&root));
59
60    let app_state = Arc::new(match config.ito_path {
61        Some(ito_path) => AppState::with_ito_path(root.clone(), ito_path),
62        None => AppState::new(root.clone()),
63    });
64
65    let auth_state = Arc::new(AuthState {
66        token: token.clone(),
67    });
68
69    // Build CORS layer
70    let cors = match config.cors_origins {
71        Some(origins) => {
72            let mut layer = CorsLayer::new();
73            for origin in origins {
74                let Ok(origin): Result<axum::http::HeaderValue, _> = origin.parse() else {
75                    eprintln!("warning: invalid CORS origin skipped: {origin}");
76                    continue;
77                };
78                layer = layer.allow_origin(origin);
79            }
80            layer
81        }
82        None => CorsLayer::permissive(),
83    };
84
85    let app = Router::new()
86        .nest("/api/v1", api::v1_router())
87        .with_state(app_state)
88        .layer(middleware::from_fn_with_state(
89            auth_state,
90            auth::auth_middleware,
91        ))
92        .layer(cors);
93
94    let addr: SocketAddr = format!("{}:{}", config.bind, config.port)
95        .parse()
96        .map_err(|e| miette::miette!("Invalid address: {e}"))?;
97
98    let listener = tokio::net::TcpListener::bind(addr)
99        .await
100        .map_err(|e| miette::miette!("Failed to bind to {addr}: {e}"))?;
101
102    eprintln!("ito-backend serving {} at http://{addr}/", root.display());
103    eprintln!("Auth token: {token}");
104
105    axum::serve(listener, app)
106        .await
107        .map_err(|e| miette::miette!("Server error: {e}"))?;
108
109    Ok(())
110}