diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 6635972..bbb4b6a 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -91,6 +91,15 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -193,6 +202,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -416,6 +431,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if 1.0.4", +] + [[package]] name = "crossbeam-deque" version = "0.7.4" @@ -480,6 +504,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -612,6 +647,16 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "miniz_oxide", + "zlib-rs", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1398,6 +1443,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1553,6 +1599,8 @@ dependencies = [ "tokio 1.49.0", "tower", "tower-http", + "utoipa", + "utoipa-swagger-ui", ] [[package]] @@ -2032,7 +2080,7 @@ version = "0.10.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" dependencies = [ - "base64", + "base64 0.13.1", "bytes 0.5.6", "encoding_rs", "futures-core", @@ -2113,6 +2161,40 @@ dependencies = [ "rusqlite", ] +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -2163,6 +2245,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.28" @@ -2351,6 +2442,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" version = "0.4.11" @@ -2985,6 +3082,48 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "9.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" +dependencies = [ + "axum", + "base64 0.22.1", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "url", + "utoipa", + "zip", +] + [[package]] name = "uuid" version = "1.21.0" @@ -3009,6 +3148,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3175,6 +3324,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -3553,8 +3711,40 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.13.0", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + [[package]] name = "zmij" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 6c16a9e..515c79e 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -26,3 +26,5 @@ r2d2 = "0.8.10" r2d2_sqlite = "0.32.0" once_cell = "1.21.3" tower = "0.5.3" +utoipa = { version = "5", features = ["axum_extras", "chrono"] } +utoipa-swagger-ui = { version = "9", features = ["axum"] } diff --git a/backend/src/model/devices.rs b/backend/src/model/devices.rs index c6ef6b5..5248936 100644 --- a/backend/src/model/devices.rs +++ b/backend/src/model/devices.rs @@ -2,13 +2,15 @@ use crate::utils::date_serializer; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::fmt; +use utoipa::ToSchema; -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct Device { pub mac_address: String, pub ipv4_address: String, pub vendor: String, #[serde(with = "date_serializer")] + #[schema(value_type = String, format = DateTime)] pub last_seen: DateTime, pub is_registered: bool, pub owner: String, diff --git a/backend/src/model/notifications.rs b/backend/src/model/notifications.rs index 45a9ec0..c2d8a97 100644 --- a/backend/src/model/notifications.rs +++ b/backend/src/model/notifications.rs @@ -3,11 +3,13 @@ use chrono::{DateTime, Utc}; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; use serde::{Deserialize, Serialize}; use std::{error::Error, fmt, str::FromStr}; +use utoipa::ToSchema; -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct Notification { pub id: i64, #[serde(with = "date_serializer")] + #[schema(value_type = String, format = DateTime)] pub created_on: DateTime, pub notification_type: NotificationType, pub title: String, @@ -50,7 +52,7 @@ impl PartialEq for Notification { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema)] pub enum NotificationType { NewDeviceFound, DeviceOnlineAfterTime, diff --git a/backend/src/web_server.rs b/backend/src/web_server.rs index 9b1ee09..17286fb 100644 --- a/backend/src/web_server.rs +++ b/backend/src/web_server.rs @@ -1,6 +1,9 @@ use std::error::Error; +use crate::model::devices::Device; +use crate::model::notifications::{Notification, NotificationType}; use crate::settings::get_settings; +use crate::web_server::devices::RegisterDevicePayload; use axum::extract::Request; use axum::http::StatusCode; use axum::middleware::Next; @@ -12,11 +15,64 @@ use tower::ServiceBuilder; use tower_http::cors::{Any, CorsLayer}; use tower_http::services::ServeDir; use axum::Json; +use utoipa::OpenApi; +use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme}; +use utoipa::Modify; +use utoipa_swagger_ui::SwaggerUi; pub mod devices; pub mod notifications; pub mod utils; +#[derive(OpenApi)] +#[openapi( + info( + title = "OOTT API", + version = "0.1.0", + description = "Network monitoring and alert system API" + ), + paths( + test_api, + devices::list, + devices::read, + devices::register, + devices::unregister, + notifications::list, + notifications::read, + notifications::read_without_flagging, + notifications::mark_as_new, + notifications::mark_all_as_old, + ), + components(schemas( + Device, + Notification, + NotificationType, + RegisterDevicePayload, + )), + modifiers(&SecurityAddon), + tags( + (name = "devices", description = "Device management"), + (name = "notifications", description = "Notification management"), + ) +)] +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", + SecurityScheme::Http( + HttpBuilder::new() + .scheme(HttpAuthScheme::Bearer) + .build(), + ), + ); + } +} + pub async fn serve() -> Result<(), Box> { info!("Starting web server"); let static_files = ServeDir::new("./web"); @@ -52,7 +108,8 @@ pub async fn serve() -> Result<(), Box> { "/", get(|| async { "Go to /web for the UI or to /api for the better UI." }), ) - .nest_service("/web", static_files); + .nest_service("/web", static_files) + .merge(SwaggerUi::new("/api/docs").url("/api/docs/openapi.json", ApiDoc::openapi())); let web_server_host_and_port = format!( "{}:{}", @@ -70,6 +127,14 @@ pub async fn serve() -> Result<(), Box> { Ok(()) } +#[utoipa::path( + get, + path = "/api/test", + responses( + (status = 200, description = "API is reachable", body = String), + ), + security(("bearer_auth" = [])) +)] async fn test_api() -> Result, StatusCode> { Ok(Json("OOTT_API_OK".to_string())) } diff --git a/backend/src/web_server/devices.rs b/backend/src/web_server/devices.rs index fe8132a..eb80d88 100644 --- a/backend/src/web_server/devices.rs +++ b/backend/src/web_server/devices.rs @@ -6,11 +6,30 @@ use axum::{Json, extract::Query, http::StatusCode}; use chrono::{DateTime, Utc}; use log::{debug, error}; use serde::Deserialize; +use utoipa::ToSchema; use crate::{db, model::devices::Device}; use crate::web_server::utils; +#[utoipa::path( + get, + path = "/api/devices", + tag = "devices", + params( + ("is_registered" = Option, Query, description = "Filter by registration status"), + ("last_seen_from" = Option, Query, description = "Filter devices seen after this datetime (RFC3339)"), + ("last_seen_to" = Option, Query, description = "Filter devices seen before this datetime (RFC3339)"), + ("owner" = Option, Query, description = "Filter by owner"), + ("device_type" = Option, Query, description = "Filter by device type"), + ("vendor" = Option, Query, description = "Filter by vendor"), + ), + responses( + (status = 200, description = "List of devices", body = Vec), + (status = 500, description = "Internal server error"), + ), + security(("bearer_auth" = [])) +)] pub async fn list( Query(params): Query>, ) -> Result>, StatusCode> { @@ -38,6 +57,19 @@ pub async fn list( } } +#[utoipa::path( + get, + path = "/api/devices/{mac_address}", + tag = "devices", + params( + ("mac_address" = String, Path, description = "MAC address of the device"), + ), + responses( + (status = 200, description = "Device found", body = Device), + (status = 404, description = "Device not found"), + ), + security(("bearer_auth" = [])) +)] pub async fn read(Path(mac_address): Path) -> Result, StatusCode> { match db::devices::read(mac_address) { Some(value) => Ok(Json(value)), @@ -45,6 +77,19 @@ pub async fn read(Path(mac_address): Path) -> Result, Statu } } +#[utoipa::path( + put, + path = "/api/devices", + tag = "devices", + request_body = RegisterDevicePayload, + responses( + (status = 201, description = "Device registered"), + (status = 404, description = "Device not found"), + (status = 409, description = "Device already registered"), + (status = 500, description = "Internal server error"), + ), + security(("bearer_auth" = [])) +)] pub async fn register(Json(payload): Json) -> impl IntoResponse { debug!( "Device registration received: mac_address={}, owner={}, device_type={}", @@ -84,6 +129,21 @@ pub async fn register(Json(payload): Json) -> impl IntoRe } } +#[utoipa::path( + delete, + path = "/api/devices/{mac_address}", + tag = "devices", + params( + ("mac_address" = String, Path, description = "MAC address of the device"), + ), + responses( + (status = 200, description = "Device unregistered"), + (status = 404, description = "Device not found"), + (status = 409, description = "Device is not registered"), + (status = 500, description = "Internal server error"), + ), + security(("bearer_auth" = [])) +)] pub async fn unregister(Path(mac_address): Path) -> impl IntoResponse { let mut device = match db::devices::read(mac_address) { Some(value) => value, @@ -119,7 +179,7 @@ pub async fn unregister(Path(mac_address): Path) -> impl IntoResponse { } // Payload structs -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] pub struct RegisterDevicePayload { mac_address: String, owner: String, diff --git a/backend/src/web_server/notifications.rs b/backend/src/web_server/notifications.rs index f021f17..dffd4f5 100644 --- a/backend/src/web_server/notifications.rs +++ b/backend/src/web_server/notifications.rs @@ -10,6 +10,20 @@ use log::error; use crate::{db, model::notifications::Notification, web_server::utils}; +#[utoipa::path( + get, + path = "/api/notifications/{id}", + tag = "notifications", + params( + ("id" = i64, Path, description = "Notification ID"), + ), + responses( + (status = 200, description = "Notification found and marked as read", body = Notification), + (status = 404, description = "Notification not found"), + (status = 500, description = "Internal server error"), + ), + security(("bearer_auth" = [])) +)] pub async fn read(Path(id): Path) -> Result, StatusCode> { match db::notifications::mark_as_old(id) { Ok(_) => {} @@ -25,6 +39,19 @@ pub async fn read(Path(id): Path) -> Result, StatusCode> } } +#[utoipa::path( + get, + path = "/api/notifications/{id}/read_without_flagging", + tag = "notifications", + params( + ("id" = i64, Path, description = "Notification ID"), + ), + responses( + (status = 200, description = "Notification found without marking as read", body = Notification), + (status = 404, description = "Notification not found"), + ), + security(("bearer_auth" = [])) +)] pub async fn read_without_flagging(Path(id): Path) -> Result, StatusCode> { match db::notifications::read(id) { Some(value) => Ok(Json(value)), @@ -32,6 +59,19 @@ pub async fn read_without_flagging(Path(id): Path) -> Result) -> impl IntoResponse { match db::notifications::mark_as_new(id) { Ok(_) => (StatusCode::OK, "Notification marked as new"), @@ -45,6 +85,16 @@ pub async fn mark_as_new(Path(id): Path) -> impl IntoResponse { } } +#[utoipa::path( + post, + path = "/api/notifications/mark_all_as_old", + tag = "notifications", + responses( + (status = 200, description = "All notifications marked as old"), + (status = 500, description = "Internal server error"), + ), + security(("bearer_auth" = [])) +)] pub async fn mark_all_as_old() -> impl IntoResponse { match db::notifications::mark_all_as_old() { Ok(_) => (StatusCode::OK, "All notifications marked as old"), @@ -58,6 +108,21 @@ pub async fn mark_all_as_old() -> impl IntoResponse { } } +#[utoipa::path( + get, + path = "/api/notifications", + tag = "notifications", + params( + ("is_new" = Option, Query, description = "Filter by new/read status"), + ("page_offset" = Option, Query, description = "Pagination offset"), + ("page_limit" = Option, Query, description = "Maximum number of results to return"), + ), + responses( + (status = 200, description = "List of notifications", body = Vec), + (status = 500, description = "Internal server error"), + ), + security(("bearer_auth" = [])) +)] pub async fn list( Query(params): Query>, ) -> Result>, StatusCode> {