Skip to main content

ito_backend/
server.rs

1//! HTTP server bootstrap and route assembly for the multi-tenant backend.
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 ito_config::types::{BackendServerConfig, BackendStorageKind};
14use ito_core::BackendProjectStore;
15use ito_core::fs_project_store::FsBackendProjectStore;
16use ito_core::sqlite_project_store::SqliteBackendProjectStore;
17
18use crate::api;
19use crate::auth;
20use crate::state::AppState;
21
22/// Resolve the data directory from the server config.
23///
24/// Priority: explicit `data_dir` in config → `$XDG_DATA_HOME/ito/backend`
25/// → `$HOME/.local/share/ito/backend`.
26fn resolve_data_dir(config: &BackendServerConfig) -> miette::Result<PathBuf> {
27    if let Some(dir) = &config.data_dir {
28        return Ok(PathBuf::from(dir));
29    }
30
31    // XDG_DATA_HOME fallback
32    if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
33        return Ok(PathBuf::from(xdg).join("ito").join("backend"));
34    }
35
36    // $HOME fallback
37    let home = std::env::var("HOME").map_err(|_| {
38        miette::miette!(
39            "Cannot determine data directory: neither data_dir, XDG_DATA_HOME, nor HOME is set"
40        )
41    })?;
42
43    Ok(PathBuf::from(home)
44        .join(".local")
45        .join("share")
46        .join("ito")
47        .join("backend"))
48}
49
50/// Build the project store based on configuration.
51///
52/// Selects between filesystem and SQLite backends.
53fn build_project_store(
54    config: &BackendServerConfig,
55    data_dir: &PathBuf,
56) -> miette::Result<Arc<dyn BackendProjectStore>> {
57    match config.storage.kind {
58        BackendStorageKind::Filesystem => {
59            let store = FsBackendProjectStore::new(data_dir);
60            Ok(Arc::new(store))
61        }
62        BackendStorageKind::Sqlite => {
63            let db_path = match &config.storage.sqlite.db_path {
64                Some(path) => PathBuf::from(path),
65                None => data_dir.join("sqlite").join("ito-backend.db"),
66            };
67            let store = SqliteBackendProjectStore::open(&db_path).map_err(|e| {
68                miette::miette!("Failed to open SQLite store at {}: {e}", db_path.display())
69            })?;
70            Ok(Arc::new(store))
71        }
72    }
73}
74
75/// Start the multi-tenant backend API server and block until it shuts down.
76///
77/// Assembles routes, auth middleware, and CORS, then binds to the configured
78/// address. Prints the listening address and auth info to stderr on startup.
79pub async fn serve(config: BackendServerConfig) -> miette::Result<()> {
80    let data_dir = resolve_data_dir(&config)?;
81
82    // Ensure data directory exists
83    std::fs::create_dir_all(&data_dir).map_err(|e| {
84        miette::miette!(
85            "Failed to create data directory {}: {e}",
86            data_dir.display()
87        )
88    })?;
89
90    let data_dir = data_dir.canonicalize().unwrap_or(data_dir);
91
92    let store = build_project_store(&config, &data_dir)?;
93
94    let app_state = Arc::new(AppState::new(
95        data_dir.clone(),
96        store,
97        config.allowed.clone(),
98        config.auth.clone(),
99    ));
100
101    // Build CORS layer
102    let cors = match &config.cors.origins {
103        Some(origins) => {
104            let mut layer = CorsLayer::new();
105            for origin in origins {
106                let Ok(header_val) = origin.parse::<axum::http::HeaderValue>() else {
107                    eprintln!("warning: invalid CORS origin skipped: {origin}");
108                    continue;
109                };
110                layer = layer.allow_origin(header_val);
111            }
112            layer
113        }
114        None => CorsLayer::permissive(),
115    };
116
117    let app = Router::new()
118        .nest("/api/v1", api::v1_router())
119        .with_state(app_state.clone())
120        .layer(middleware::from_fn_with_state(
121            app_state,
122            auth::auth_middleware,
123        ))
124        .layer(cors);
125
126    let addr: SocketAddr = format!("{}:{}", config.bind, config.port)
127        .parse()
128        .map_err(|e| miette::miette!("Invalid address: {e}"))?;
129
130    let listener = tokio::net::TcpListener::bind(addr)
131        .await
132        .map_err(|e| miette::miette!("Failed to bind to {addr}: {e}"))?;
133
134    let admin_count = config.auth.admin_tokens.len();
135    let has_seed = config.auth.token_seed.is_some();
136    let allowed_orgs = config.allowed.orgs.len();
137    let storage_kind = match config.storage.kind {
138        BackendStorageKind::Filesystem => "filesystem",
139        BackendStorageKind::Sqlite => "sqlite",
140    };
141
142    eprintln!("ito-backend (multi-tenant) listening at http://{addr}/");
143    eprintln!("  data_dir: {}", data_dir.display());
144    eprintln!("  storage: {storage_kind}");
145    eprintln!("  admin_tokens: {admin_count}, token_seed: {has_seed}");
146    eprintln!("  allowed orgs: {allowed_orgs}");
147
148    axum::serve(listener, app)
149        .await
150        .map_err(|e| miette::miette!("Server error: {e}"))?;
151
152    Ok(())
153}