1use axum::{Router, middleware, routing::get};
9use std::net::SocketAddr;
10use std::path::PathBuf;
11use std::sync::Arc;
12use tower_http::cors::CorsLayer;
13
14use crate::api;
15use crate::auth::{self, AuthState};
16use crate::frontend;
17use crate::terminal::{self, TerminalState};
18
19#[derive(Debug, Clone)]
21pub struct ServeConfig {
22 pub root: PathBuf,
24 pub bind: String,
26 pub port: u16,
28}
29
30impl Default for ServeConfig {
31 fn default() -> Self {
32 Self {
33 root: PathBuf::from("."),
34 bind: "127.0.0.1".to_string(),
35 port: 9009,
36 }
37 }
38}
39
40pub async fn serve(config: ServeConfig) -> miette::Result<()> {
45 let root = config.root.canonicalize().unwrap_or(config.root.clone());
46
47 let token = if auth::is_loopback(&config.bind) {
49 None
50 } else {
51 Some(auth::generate_token(&root))
52 };
53
54 let auth_state = Arc::new(AuthState {
55 token: token.clone(),
56 });
57 let terminal_state = Arc::new(TerminalState { root: root.clone() });
58
59 let app = Router::new()
60 .route("/", get(frontend::index))
62 .route("/app.js", get(frontend::app_js))
63 .route("/ws/terminal", get(terminal::ws_handler))
65 .with_state(terminal_state)
66 .nest("/api", api::router(root.clone()))
68 .layer(middleware::from_fn_with_state(
70 auth_state,
71 auth::auth_middleware,
72 ))
73 .layer(CorsLayer::permissive());
75
76 let addr: SocketAddr = format!("{}:{}", config.bind, config.port)
77 .parse()
78 .map_err(|e| miette::miette!("Invalid address: {e}"))?;
79
80 let listener = tokio::net::TcpListener::bind(addr)
81 .await
82 .map_err(|e| miette::miette!("Failed to bind to {addr}: {e}"))?;
83
84 let url = if let Some(t) = &token {
86 format!("http://{addr}/?token={t}")
87 } else {
88 format!("http://{addr}/")
89 };
90 println!("Serving {} at {url}", root.display());
91
92 axum::serve(listener, app)
93 .await
94 .map_err(|e| miette::miette!("Server error: {e}"))?;
95
96 Ok(())
97}