use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{info, warn};
pub mod auth;
pub mod handlers;
pub mod routes;
pub mod state;
pub use auth::AuthConfig;
pub use routes::create_router;
pub use state::AppState;
pub async fn start_dashboard(port: u16, state: AppState) -> anyhow::Result<()> {
let bind_host = std::env::var("ISELF_BIND")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "127.0.0.1".to_string());
let mut auth_cfg = AuthConfig::from_env_or_disk();
let is_loopback = bind_host == "127.0.0.1" || bind_host == "::1" || bind_host == "localhost";
if !is_loopback && auth_cfg.token.is_none() {
match autogenerate_api_token() {
Ok((token, path)) => {
warn!(
"No API token configured for non-loopback bind. Auto-generated one at {} \
(chmod 600). Use it as Authorization: Bearer <token>, or open the dashboard \
once with ?token=<token> in the URL to bootstrap the browser session.",
path.display()
);
println!("\n ISELF_API_TOKEN={}\n", token);
auth_cfg = AuthConfig { token: Some(token) };
}
Err(e) => {
anyhow::bail!(
"Refusing to bind to non-loopback address {} without an API token, \
and could not auto-generate one ({}). Set ISELF_API_TOKEN=<token> \
(or write the token to ~/.i-self/api_token) before starting.",
bind_host,
e
);
}
}
}
info!("Starting i-self web dashboard on {}:{}", bind_host, port);
if auth_cfg.token.is_some() {
info!("Bearer-token auth: ENABLED — requests to /api/* must carry `Authorization: Bearer <token>`");
} else if is_loopback {
warn!("Bearer-token auth: DISABLED (loopback only). Any local process can call /api/*.");
}
let app = create_router(Arc::new(RwLock::new(state)), Arc::new(auth_cfg));
let listener = tokio::net::TcpListener::bind(format!("{}:{}", bind_host, port)).await?;
info!("Dashboard available at http://{}:{}", bind_host, port);
axum::serve(listener, app).await?;
Ok(())
}
fn autogenerate_api_token() -> anyhow::Result<(String, std::path::PathBuf)> {
use rand::RngCore;
let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("no home directory"))?;
let dir = home.join(".i-self");
std::fs::create_dir_all(&dir)?;
let path = dir.join("api_token");
let mut bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut bytes);
let token = hex_encode(&bytes);
std::fs::write(&path, &token)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&path)?.permissions();
perms.set_mode(0o600);
std::fs::set_permissions(&path, perms)?;
}
Ok((token, path))
}
fn hex_encode(b: &[u8]) -> String {
const H: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(b.len() * 2);
for &byte in b {
s.push(H[(byte >> 4) as usize] as char);
s.push(H[(byte & 0x0f) as usize] as char);
}
s
}