1use 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
22fn 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 if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
33 return Ok(PathBuf::from(xdg).join("ito").join("backend"));
34 }
35
36 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
50fn 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
75pub async fn serve(config: BackendServerConfig) -> miette::Result<()> {
80 let data_dir = resolve_data_dir(&config)?;
81
82 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 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}