Files
labdeck/src/main.rs
T
2026-10-27 11:32:37 +01:00

104 lines
3.4 KiB
Rust

mod alerts;
mod api;
mod auth;
mod db;
mod health;
mod term;
mod vault;
mod web;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
pub struct App {
pub db: Mutex<rusqlite::Connection>,
/// Derived vault key; only present while at least one session is logged in.
pub vault_key: RwLock<Option<vault::VaultKey>>,
/// session token -> expiry (unix seconds)
pub sessions: Mutex<HashMap<String, i64>>,
/// single-use websocket tickets -> expiry
pub tickets: Mutex<HashMap<String, i64>>,
pub live: RwLock<HashMap<i64, health::Live>>,
pub login_guard: Mutex<auth::LoginGuard>,
pub http_strict: reqwest::Client,
pub http_insecure: reqwest::Client,
pub wake: tokio::sync::Notify,
pub started: i64,
}
fn build_client(insecure: bool) -> reqwest::Client {
reqwest::Client::builder()
.user_agent(concat!("labdeck/", env!("CARGO_PKG_VERSION")))
.redirect(reqwest::redirect::Policy::limited(5))
.danger_accept_invalid_certs(insecure)
.tls_info(true)
.pool_max_idle_per_host(0)
.build()
.expect("http client")
}
#[tokio::main]
async fn main() {
let addr: SocketAddr = std::env::var("LABDECK_ADDR")
.unwrap_or_else(|_| "127.0.0.1:8787".into())
.parse()
.expect("LABDECK_ADDR must look like 127.0.0.1:8787");
let data_dir = std::env::var("LABDECK_DATA").unwrap_or_else(|_| "./data".into());
std::fs::create_dir_all(&data_dir).expect("cannot create data dir");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&data_dir, std::fs::Permissions::from_mode(0o700));
}
let db_path = format!("{}/labdeck.db", data_dir.trim_end_matches('/'));
let conn = db::open(&db_path).unwrap_or_else(|e| {
eprintln!("cannot open database {db_path}: {e}");
eprintln!("hint: the data directory must be writable by this process.");
eprintln!(" Docker bind mount: sudo chown 10001:10001 <dir> (or use a named volume)");
std::process::exit(1);
});
let app = Arc::new(App {
db: Mutex::new(conn),
vault_key: RwLock::new(None),
sessions: Mutex::new(HashMap::new()),
tickets: Mutex::new(HashMap::new()),
live: RwLock::new(HashMap::new()),
login_guard: Mutex::new(auth::LoginGuard::default()),
http_strict: build_client(false),
http_insecure: build_client(true),
wake: tokio::sync::Notify::new(),
started: db::now(),
});
let router = api::router(app.clone());
let listener = tokio::net::TcpListener::bind(addr)
.await
.unwrap_or_else(|e| panic!("cannot bind {addr}: {e}"));
// start background work only once we are reachable (self-checks, alerts)
tokio::spawn(health::scheduler(app.clone()));
tokio::spawn(auth::janitor(app.clone()));
eprintln!(
"labdeck {} listening on http://{} (data: {})",
env!("CARGO_PKG_VERSION"),
addr,
db_path
);
if !addr.ip().is_loopback() {
eprintln!("note: not bound to loopback - put this behind HTTPS (reverse proxy / tunnel).");
}
axum::serve(listener, router)
.with_graceful_shutdown(async {
let _ = tokio::signal::ctrl_c().await;
eprintln!("shutting down");
tokio::time::sleep(Duration::from_millis(100)).await;
})
.await
.expect("server error");
}