1pub mod errors;
2pub mod handlers;
3pub mod routes;
4pub mod state;
5pub mod types;
6pub mod watcher;
7pub mod ws;
8
9use std::net::SocketAddr;
10use std::path::PathBuf;
11
12use anyhow::Result;
13use axum::extract::DefaultBodyLimit;
14use axum::http::{Request, StatusCode};
15use axum::middleware::{self, Next};
16use axum::response::Response;
17use tower_http::cors::CorsLayer;
18
19use crate::db::Database;
20use state::AppState;
21
22const MAX_BODY_SIZE: usize = 10 * 1024 * 1024;
24
25async fn auth_middleware(
31 axum::extract::State(state): axum::extract::State<AppState>,
32 request: Request<axum::body::Body>,
33 next: Next,
34) -> Result<Response, StatusCode> {
35 let path = request.uri().path();
36
37 if path == "/api/v1/health" || path == "/ws" || !path.starts_with("/api/") {
39 return Ok(next.run(request).await);
40 }
41
42 let authorized = request
43 .headers()
44 .get("authorization")
45 .and_then(|v| v.to_str().ok())
46 .and_then(|v| v.strip_prefix("Bearer "))
47 .is_some_and(|token| token == state.auth_token);
48
49 if authorized {
50 Ok(next.run(request).await)
51 } else {
52 Err(StatusCode::UNAUTHORIZED)
53 }
54}
55
56pub async fn run(
69 port: u16,
70 dashboard_dir: Option<PathBuf>,
71 db: Database,
72 crosslink_dir: PathBuf,
73) -> Result<()> {
74 let state = AppState::new(db, crosslink_dir.clone());
75
76 watcher::start_watcher(crosslink_dir, state.ws_tx.clone());
78
79 let localhost: axum::http::HeaderValue = "http://localhost:5173".parse()?;
84 let loopback: axum::http::HeaderValue = "http://127.0.0.1:5173".parse()?;
85 let cors = CorsLayer::new()
86 .allow_origin([localhost, loopback])
87 .allow_methods(tower_http::cors::Any)
88 .allow_headers([
89 axum::http::header::CONTENT_TYPE,
90 axum::http::header::AUTHORIZATION,
91 axum::http::header::ACCEPT,
92 ]);
93
94 let has_dashboard = dashboard_dir.is_some();
97
98 let app = routes::build_router(state.clone(), dashboard_dir)
99 .layer(middleware::from_fn_with_state(
100 state.clone(),
101 auth_middleware,
102 ))
103 .layer(DefaultBodyLimit::max(MAX_BODY_SIZE))
104 .layer(cors);
105
106 let addr = SocketAddr::from(([127, 0, 0, 1], port));
107 println!("crosslink serve: listening on http://{addr}");
108 if has_dashboard {
109 println!(" Dashboard: http://{addr}/?token={}", state.auth_token);
114 }
115 println!(" API: http://{addr}/api/v1/health");
116 println!(" WebSocket: ws://{addr}/ws");
117 println!(" Auth: Bearer {}", state.auth_token);
118
119 let listener = tokio::net::TcpListener::bind(addr).await?;
120 axum::serve(listener, app).await?;
121
122 Ok(())
123}