2026-02-26 08:31:48 -05:00
|
|
|
use std::error::Error;
|
2026-06-01 21:43:08 -04:00
|
|
|
use std::path::{Path, PathBuf};
|
2026-02-23 09:57:22 -05:00
|
|
|
|
2026-06-01 08:09:34 -04:00
|
|
|
use crate::model::device_events::{DeviceEvent, DeviceEventScanner, DeviceEventType};
|
2026-06-05 21:09:25 -04:00
|
|
|
use crate::model::devices::{Device, DeviceListResponse, DeviceSummary};
|
|
|
|
|
use crate::model::notifications::{Notification, NotificationListResponse, NotificationType};
|
2026-02-26 16:30:51 -05:00
|
|
|
use crate::settings::get_settings;
|
2026-05-30 09:27:44 -04:00
|
|
|
use crate::web_server::devices::{RegisterDevicePayload, UpdateDevicePayload};
|
2026-06-02 10:14:50 -04:00
|
|
|
use crate::web_server::scanner_status::{
|
|
|
|
|
ActiveScannerStatusResponse, PassiveScannerStatusResponse,
|
|
|
|
|
};
|
2026-05-29 09:37:33 -04:00
|
|
|
use axum::Json;
|
2026-02-26 12:19:58 -05:00
|
|
|
use axum::extract::Request;
|
|
|
|
|
use axum::http::StatusCode;
|
|
|
|
|
use axum::middleware::Next;
|
|
|
|
|
use axum::response::Response;
|
2026-05-27 10:13:56 -04:00
|
|
|
use axum::routing::{delete, get, post, put};
|
2026-02-27 12:28:16 -05:00
|
|
|
use axum::{Router, http};
|
2026-02-26 16:30:51 -05:00
|
|
|
use log::{debug, error, info};
|
2026-02-27 12:28:16 -05:00
|
|
|
use tower::ServiceBuilder;
|
|
|
|
|
use tower_http::cors::{Any, CorsLayer};
|
2026-02-17 09:34:22 -05:00
|
|
|
use tower_http::services::ServeDir;
|
2026-05-29 09:37:33 -04:00
|
|
|
use utoipa::Modify;
|
2026-05-27 13:36:05 -04:00
|
|
|
use utoipa::OpenApi;
|
|
|
|
|
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
|
|
|
|
|
use utoipa_swagger_ui::SwaggerUi;
|
2026-02-17 09:34:22 -05:00
|
|
|
|
2026-05-28 17:57:26 -04:00
|
|
|
pub mod arp_scanner;
|
2026-05-27 21:22:13 -04:00
|
|
|
pub mod device_events;
|
2026-02-26 08:31:48 -05:00
|
|
|
pub mod devices;
|
2026-06-01 18:34:02 -04:00
|
|
|
pub mod dhcp_scanner;
|
2026-05-29 09:37:33 -04:00
|
|
|
pub mod mdns_scanner;
|
2026-02-26 08:31:48 -05:00
|
|
|
pub mod notifications;
|
2026-06-02 10:14:50 -04:00
|
|
|
pub mod scanner_status;
|
2026-06-02 08:03:20 -04:00
|
|
|
pub mod snmp_scanner;
|
2026-06-01 17:55:33 -04:00
|
|
|
pub mod ssdp_scanner;
|
2026-02-26 08:31:48 -05:00
|
|
|
pub mod utils;
|
2026-02-25 15:58:32 -05:00
|
|
|
|
2026-05-27 13:36:05 -04:00
|
|
|
#[derive(OpenApi)]
|
|
|
|
|
#[openapi(
|
|
|
|
|
info(
|
|
|
|
|
title = "OOTT API",
|
2026-06-02 16:49:52 -04:00
|
|
|
// version is intentionally omitted: utoipa fills it from the crate
|
|
|
|
|
// version (CARGO_PKG_VERSION, i.e. backend/Cargo.toml) automatically.
|
2026-05-27 13:36:05 -04:00
|
|
|
description = "Network monitoring and alert system API"
|
|
|
|
|
),
|
|
|
|
|
paths(
|
|
|
|
|
test_api,
|
|
|
|
|
devices::list,
|
2026-05-28 19:10:08 -04:00
|
|
|
devices::summary,
|
2026-05-27 13:36:05 -04:00
|
|
|
devices::read,
|
|
|
|
|
devices::register,
|
2026-05-30 09:27:44 -04:00
|
|
|
devices::update,
|
2026-05-27 13:36:05 -04:00
|
|
|
devices::unregister,
|
|
|
|
|
notifications::list,
|
|
|
|
|
notifications::read,
|
|
|
|
|
notifications::read_without_flagging,
|
|
|
|
|
notifications::mark_as_new,
|
|
|
|
|
notifications::mark_all_as_old,
|
2026-05-27 21:22:13 -04:00
|
|
|
device_events::list,
|
2026-05-28 17:57:26 -04:00
|
|
|
arp_scanner::status,
|
2026-05-29 09:37:33 -04:00
|
|
|
mdns_scanner::status,
|
2026-06-01 17:55:33 -04:00
|
|
|
ssdp_scanner::status,
|
2026-06-01 18:34:02 -04:00
|
|
|
dhcp_scanner::status,
|
2026-06-02 08:03:20 -04:00
|
|
|
snmp_scanner::status,
|
2026-05-27 13:36:05 -04:00
|
|
|
),
|
|
|
|
|
components(schemas(
|
|
|
|
|
Device,
|
2026-06-05 21:09:25 -04:00
|
|
|
DeviceListResponse,
|
2026-05-28 19:10:08 -04:00
|
|
|
DeviceSummary,
|
2026-05-27 13:36:05 -04:00
|
|
|
Notification,
|
2026-06-05 21:09:25 -04:00
|
|
|
NotificationListResponse,
|
2026-05-27 13:36:05 -04:00
|
|
|
NotificationType,
|
|
|
|
|
RegisterDevicePayload,
|
2026-05-30 09:27:44 -04:00
|
|
|
UpdateDevicePayload,
|
2026-05-27 21:22:13 -04:00
|
|
|
DeviceEvent,
|
|
|
|
|
DeviceEventType,
|
2026-06-01 08:09:34 -04:00
|
|
|
DeviceEventScanner,
|
2026-06-02 10:14:50 -04:00
|
|
|
ActiveScannerStatusResponse,
|
|
|
|
|
PassiveScannerStatusResponse,
|
2026-05-27 13:36:05 -04:00
|
|
|
)),
|
|
|
|
|
modifiers(&SecurityAddon),
|
|
|
|
|
tags(
|
|
|
|
|
(name = "devices", description = "Device management"),
|
|
|
|
|
(name = "notifications", description = "Notification management"),
|
2026-05-27 21:22:13 -04:00
|
|
|
(name = "device_events", description = "Device event history"),
|
2026-05-28 17:57:26 -04:00
|
|
|
(name = "arp_scanner", description = "ARP scanner process status"),
|
2026-05-29 09:37:33 -04:00
|
|
|
(name = "mdns_scanner", description = "mDNS/Bonjour scanner process status"),
|
2026-06-01 17:55:33 -04:00
|
|
|
(name = "ssdp_scanner", description = "SSDP/UPnP scanner process status"),
|
2026-06-01 18:34:02 -04:00
|
|
|
(name = "dhcp_scanner", description = "DHCP scanner process status"),
|
2026-06-02 08:03:20 -04:00
|
|
|
(name = "snmp_scanner", description = "SNMP scanner process status"),
|
2026-05-27 13:36:05 -04:00
|
|
|
)
|
|
|
|
|
)]
|
|
|
|
|
struct ApiDoc;
|
|
|
|
|
|
|
|
|
|
struct SecurityAddon;
|
|
|
|
|
|
|
|
|
|
impl Modify for SecurityAddon {
|
|
|
|
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
|
|
|
|
let components = openapi.components.get_or_insert_with(Default::default);
|
|
|
|
|
components.add_security_scheme(
|
|
|
|
|
"bearer_auth",
|
2026-05-29 09:37:33 -04:00
|
|
|
SecurityScheme::Http(HttpBuilder::new().scheme(HttpAuthScheme::Bearer).build()),
|
2026-05-27 13:36:05 -04:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 21:43:08 -04:00
|
|
|
/// Resolve the directory that holds the bundled front-end (Flutter web) assets.
|
|
|
|
|
///
|
|
|
|
|
/// The assets are installed next to the binary at `<exe_dir>/../share/oott/web`
|
|
|
|
|
/// (see the Nix package definition). When the executable location cannot be
|
|
|
|
|
/// determined we fall back to `./web` relative to the current directory.
|
|
|
|
|
fn resolve_web_root(exe_dir: Option<&Path>) -> PathBuf {
|
|
|
|
|
match exe_dir {
|
|
|
|
|
Some(dir) => dir.join("../share/oott/web"),
|
|
|
|
|
None => PathBuf::from("./web"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 09:57:22 -05:00
|
|
|
pub async fn serve() -> Result<(), Box<dyn Error>> {
|
2026-02-17 09:34:22 -05:00
|
|
|
info!("Starting web server");
|
2026-06-01 21:43:08 -04:00
|
|
|
let exe = std::env::current_exe().ok();
|
|
|
|
|
let web_root = resolve_web_root(exe.as_deref().and_then(Path::parent));
|
|
|
|
|
info!("Serving front-end assets from {}", web_root.display());
|
|
|
|
|
let static_files = ServeDir::new(web_root);
|
2026-02-17 09:34:22 -05:00
|
|
|
|
2026-02-27 12:28:16 -05:00
|
|
|
// Allow all origins and headers for API
|
|
|
|
|
let cors_layer = CorsLayer::new()
|
|
|
|
|
.allow_origin(Any)
|
2026-05-27 15:33:24 -04:00
|
|
|
.allow_methods([
|
|
|
|
|
http::Method::GET,
|
|
|
|
|
http::Method::POST,
|
|
|
|
|
http::Method::PUT,
|
|
|
|
|
http::Method::DELETE,
|
|
|
|
|
])
|
2026-02-27 12:28:16 -05:00
|
|
|
.allow_headers([http::header::AUTHORIZATION, http::header::CONTENT_TYPE]);
|
|
|
|
|
|
2026-02-17 09:34:22 -05:00
|
|
|
let router = Router::new()
|
2026-03-03 12:20:57 -05:00
|
|
|
.route("/api/test", get(test_api))
|
2026-02-26 10:08:41 -05:00
|
|
|
.route("/api/devices", get(devices::list))
|
|
|
|
|
.route("/api/devices", put(devices::register))
|
2026-05-28 19:10:08 -04:00
|
|
|
.route("/api/devices/summary", get(devices::summary))
|
2026-02-26 10:08:41 -05:00
|
|
|
.route("/api/devices/{mac_address}", delete(devices::unregister))
|
|
|
|
|
.route("/api/devices/{mac_address}", get(devices::read))
|
2026-05-30 09:27:44 -04:00
|
|
|
.route("/api/devices/{mac_address}", put(devices::update))
|
2026-05-29 09:37:33 -04:00
|
|
|
.route(
|
|
|
|
|
"/api/devices/{mac_address}/events",
|
|
|
|
|
get(device_events::list),
|
|
|
|
|
)
|
2026-05-28 17:57:26 -04:00
|
|
|
.route("/api/arp_scanner/status", get(arp_scanner::status))
|
2026-05-29 09:37:33 -04:00
|
|
|
.route("/api/mdns_scanner/status", get(mdns_scanner::status))
|
2026-06-01 17:55:33 -04:00
|
|
|
.route("/api/ssdp_scanner/status", get(ssdp_scanner::status))
|
2026-06-01 18:34:02 -04:00
|
|
|
.route("/api/dhcp_scanner/status", get(dhcp_scanner::status))
|
2026-06-02 08:03:20 -04:00
|
|
|
.route("/api/snmp_scanner/status", get(snmp_scanner::status))
|
2026-02-26 10:08:41 -05:00
|
|
|
.route("/api/notifications", get(notifications::list))
|
2026-05-27 11:55:30 -04:00
|
|
|
.route(
|
|
|
|
|
"/api/notifications/mark_all_as_old",
|
|
|
|
|
post(notifications::mark_all_as_old),
|
|
|
|
|
)
|
2026-02-26 10:08:41 -05:00
|
|
|
.route("/api/notifications/{id}", get(notifications::read))
|
2026-02-23 19:45:19 -05:00
|
|
|
.route(
|
|
|
|
|
"/api/notifications/{id}/read_without_flagging",
|
2026-02-26 10:08:41 -05:00
|
|
|
get(notifications::read_without_flagging),
|
2026-02-23 19:45:19 -05:00
|
|
|
)
|
2026-05-27 10:13:56 -04:00
|
|
|
.route(
|
|
|
|
|
"/api/notifications/{id}/mark_as_new",
|
|
|
|
|
post(notifications::mark_as_new),
|
|
|
|
|
)
|
2026-02-26 12:19:58 -05:00
|
|
|
.route_layer(axum::middleware::from_fn(auth))
|
2026-02-27 12:28:16 -05:00
|
|
|
.layer(ServiceBuilder::new().layer(cors_layer))
|
2026-02-26 12:19:58 -05:00
|
|
|
.route(
|
|
|
|
|
"/",
|
|
|
|
|
get(|| async { "Go to /web for the UI or to /api for the better UI." }),
|
|
|
|
|
)
|
2026-05-27 13:36:05 -04:00
|
|
|
.nest_service("/web", static_files)
|
|
|
|
|
.merge(SwaggerUi::new("/api/docs").url("/api/docs/openapi.json", ApiDoc::openapi()));
|
2026-02-26 16:30:51 -05:00
|
|
|
|
|
|
|
|
let web_server_host_and_port = format!(
|
|
|
|
|
"{}:{}",
|
|
|
|
|
get_settings().web_server.ip_address,
|
|
|
|
|
get_settings().web_server.port
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
info!("Web server starting at http://{}", web_server_host_and_port);
|
2026-02-17 09:34:22 -05:00
|
|
|
// Start the server
|
2026-02-26 16:30:51 -05:00
|
|
|
let listener = tokio::net::TcpListener::bind(web_server_host_and_port).await?;
|
2026-02-23 09:57:22 -05:00
|
|
|
|
|
|
|
|
debug!("Web server bound to IP and port");
|
|
|
|
|
|
|
|
|
|
axum::serve(listener, router).await?;
|
2026-02-17 09:34:22 -05:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-02-26 12:19:58 -05:00
|
|
|
|
2026-05-27 13:36:05 -04:00
|
|
|
#[utoipa::path(
|
|
|
|
|
get,
|
|
|
|
|
path = "/api/test",
|
|
|
|
|
responses(
|
|
|
|
|
(status = 200, description = "API is reachable", body = String),
|
|
|
|
|
),
|
|
|
|
|
security(("bearer_auth" = []))
|
|
|
|
|
)]
|
2026-03-03 12:20:57 -05:00
|
|
|
async fn test_api() -> Result<Json<String>, StatusCode> {
|
|
|
|
|
Ok(Json("OOTT_API_OK".to_string()))
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 12:19:58 -05:00
|
|
|
async fn auth(request: Request, next: Next) -> Result<Response, StatusCode> {
|
|
|
|
|
let auth_header = request
|
|
|
|
|
.headers()
|
|
|
|
|
.get(axum::http::header::AUTHORIZATION)
|
|
|
|
|
.and_then(|header| header.to_str().ok());
|
|
|
|
|
|
|
|
|
|
let auth_header = match auth_header {
|
|
|
|
|
Some(value) => value,
|
|
|
|
|
None => return Err(StatusCode::UNAUTHORIZED),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut valid_header = "Bearer ".to_string();
|
2026-02-26 16:30:51 -05:00
|
|
|
let api_key = get_settings().web_server.api_key.as_str();
|
|
|
|
|
if api_key == "CHANGE_ME" {
|
|
|
|
|
error!("The configured api_key is still 'CHANGE_ME', please change it.");
|
|
|
|
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
valid_header.push_str(api_key);
|
2026-02-26 12:19:58 -05:00
|
|
|
|
|
|
|
|
if auth_header == valid_header {
|
|
|
|
|
Ok(next.run(request).await)
|
|
|
|
|
} else {
|
|
|
|
|
Err(StatusCode::UNAUTHORIZED)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-01 21:43:08 -04:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn web_root_is_relative_to_the_executable() {
|
|
|
|
|
let root = resolve_web_root(Some(Path::new("/nix/store/abc-oott/bin")));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
root,
|
|
|
|
|
PathBuf::from("/nix/store/abc-oott/bin/../share/oott/web")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn web_root_falls_back_to_local_dir_without_an_executable() {
|
|
|
|
|
assert_eq!(resolve_web_root(None), PathBuf::from("./web"));
|
|
|
|
|
}
|
2026-06-02 16:49:52 -04:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn openapi_version_tracks_the_crate_version() {
|
|
|
|
|
// The OpenAPI spec must report the crate version (backend/Cargo.toml)
|
|
|
|
|
// rather than a separately maintained literal.
|
|
|
|
|
let openapi = <ApiDoc as OpenApi>::openapi();
|
|
|
|
|
assert_eq!(openapi.info.version, env!("CARGO_PKG_VERSION"));
|
|
|
|
|
}
|
2026-06-01 21:43:08 -04:00
|
|
|
}
|