//! HTTP management API mounted in-process alongside hbbs's rendezvous //! listeners. The router is wired in via `src/rendezvous_server.rs`'s outer //! `tokio::select!`. M1 covers auth + heartbeat + sysinfo; later milestones //! add address book, audit, OIDC, etc. pub mod ab; pub mod admin; pub mod audit; pub mod auth; pub mod device_auth; pub mod devices_cli; pub mod email; pub mod error; pub mod groups; pub mod heartbeat; pub mod http_proxy; pub mod middleware; pub mod oidc; pub mod pagination; pub mod peers; pub mod plugin_sign; pub mod record; pub mod state; pub mod strategy; pub mod sysinfo; pub mod twofa; pub mod unattended; pub mod users; pub use state::AppState; use axum::extract::Extension; use axum::routing::{delete, get, post, put}; use axum::Router; use hbb_common::{log, ResultType}; use std::net::SocketAddr; use std::sync::Arc; pub fn router(state: Arc) -> Router { let app = Router::new() // M1: auth + heartbeat + sysinfo .route( "/api/login-options", get(auth::login_options).head(auth::login_options_head), ) .route("/api/login", post(auth::login)) .route("/api/currentUser", post(auth::current_user)) .route("/api/logout", post(auth::logout)) .route("/api/heartbeat", post(heartbeat::heartbeat)) .route("/api/sysinfo_ver", post(sysinfo::sysinfo_ver)) .route("/api/sysinfo", post(sysinfo::sysinfo)) .route( "/api/unattended-password", post(unattended::unattended_password), ) // M2: address book — modern (shared + personal) .route("/api/ab/settings", post(ab::settings::settings)) .route("/api/ab/personal", post(ab::profiles::personal)) .route( "/api/ab/shared/profiles", post(ab::profiles::shared_profiles), ) .route("/api/ab/peers", post(ab::peers::list)) .route("/api/ab/tags/:guid", post(ab::tags::list)) .route("/api/ab/peer/add/:guid", post(ab::peers::add)) .route("/api/ab/peer/update/:guid", put(ab::peers::update)) .route("/api/ab/peer/:guid", delete(ab::peers::delete)) .route("/api/ab/tag/add/:guid", post(ab::tags::add)) .route("/api/ab/tag/rename/:guid", put(ab::tags::rename)) .route("/api/ab/tag/update/:guid", put(ab::tags::update)) .route("/api/ab/tag/:guid", delete(ab::tags::delete)) // M2: address book — legacy single-blob fallback .route( "/api/ab", get(ab::legacy::get).post(ab::legacy::put), ) // M2: group / users / peers panel .route( "/api/device-group/accessible", get(groups::accessible), ) .route("/api/users", get(users::list)) .route("/api/peers", get(peers::list)) .route("/api/peers/:id/managed", put(peers::set_managed)) // M3: audit .route("/api/audit/conn", post(audit::conn::conn)) .route("/api/audit/file", post(audit::file::file)) .route("/api/audit/alarm", post(audit::alarm::alarm)) .route("/api/audit", put(audit::note::note)) // M3: session recording upload .route("/api/record", post(record::record)) // M4: TOTP enrollment (admin-only) .route("/api/2fa/enroll", post(twofa::enroll)) .route("/api/2fa/unenroll", post(twofa::unenroll)) // M4: rustdesk --assign target .route("/api/devices/cli", post(devices_cli::assign)) // M4: plugin signing (no auth — protocol-level) .route("/lic/web/api/plugin-sign", post(plugin_sign::plugin_sign)) // M4: OIDC device-flow login .route("/api/oidc/auth", post(oidc::auth::auth)) .route("/api/oidc/auth-query", get(oidc::poll::auth_query)) .route("/oidc/callback", get(oidc::callback::callback)); // M5: admin dashboard (HTMX + embedded HTML). Merged BEFORE the // Extension layer so the merged router carries the shared state. let app = match admin::build(state.clone()) { Some(admin_router) => app.merge(admin_router), None => app, }; app.layer(Extension(state)) } pub async fn serve(addr: SocketAddr, state: Arc) -> ResultType<()> { log::info!("HTTP API listening on {}", addr); let app = router(state); // Share the same router with the rendezvous-TCP HttpProxyRequest path so // both transports route through the exact same handlers. http_proxy::install_router(app.clone()); axum::Server::bind(&addr) .serve(app.into_make_service()) .await?; Ok(()) }