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