summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-07-16 23:37:24 +0300
committerDawid Rycerz <dawid@rycerz.xyz>2025-07-16 23:37:24 +0300
commitfa00c5863394c91a7b34680849908b1059e368f2 (patch)
treee357a7f322c644ce9487c4658e4a07ddb0e44611
parentaf4680ee0577b28d0563ddc3d2677e8c96f4f5eb (diff)
feat: add openapi docs generation
-rw-r--r--Cargo.lock201
-rw-r--r--Cargo.toml3
-rw-r--r--src/health.rs13
-rw-r--r--src/locations.rs3
-rw-r--r--src/main.rs358
-rw-r--r--src/notifications.rs5
-rw-r--r--src/users.rs5
-rw-r--r--src/weather_thresholds.rs3
8 files changed, 575 insertions, 16 deletions
diff --git a/Cargo.lock b/Cargo.lock
index de10021..cf8827c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -66,6 +66,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
+name = "arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
name = "async-trait"
version = "0.1.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -342,6 +351,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -397,6 +415,17 @@ dependencies = [
]
[[package]]
+name = "derive_arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "deunicode"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -528,6 +557,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
+name = "flate2"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
+dependencies = [
+ "crc32fast",
+ "libz-rs-sys",
+ "miniz_oxide",
+]
+
+[[package]]
name = "flume"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1134,6 +1174,7 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [
"equivalent",
"hashbrown 0.15.4",
+ "serde",
]
[[package]]
@@ -1237,6 +1278,15 @@ dependencies = [
]
[[package]]
+name = "libz-rs-sys"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
+dependencies = [
+ "zlib-rs",
+]
+
+[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1293,6 +1343,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1499,6 +1559,12 @@ dependencies = [
]
[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1849,6 +1915,40 @@ dependencies = [
]
[[package]]
+name = "rust-embed"
+version = "8.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
+[[package]]
name = "rustc-demangle"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2090,10 +2190,19 @@ dependencies = [
"tokio-task-scheduler",
"tower",
"tower-http",
+ "utoipa",
+ "utoipa-axum",
+ "utoipa-swagger-ui",
"uuid",
]
[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2788,6 +2897,12 @@ dependencies = [
]
[[package]]
+name = "unicase"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
+
+[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2838,6 +2953,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
+name = "utoipa"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_json",
+ "utoipa-gen",
+]
+
+[[package]]
+name = "utoipa-axum"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c25bae5bccc842449ec0c5ddc5cbb6a3a1eaeac4503895dc105a1138f8234a0"
+dependencies = [
+ "axum",
+ "paste",
+ "tower-layer",
+ "tower-service",
+ "utoipa",
+]
+
+[[package]]
+name = "utoipa-gen"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "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",
+ "mime_guess",
+ "regex",
+ "rust-embed",
+ "serde",
+ "serde_json",
+ "url",
+ "utoipa",
+ "zip",
+]
+
+[[package]]
name = "uuid"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3438,3 +3607,35 @@ dependencies = [
"quote",
"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",
+ "memchr",
+ "zopfli",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
+
+[[package]]
+name = "zopfli"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "log",
+ "simd-adler32",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 04c5cb8..5950306 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,9 @@ tokio-task-scheduler = "1.0.0"
tower = "0.5.2"
tower-http = "0.6.6"
uuid = "1.17.0"
+utoipa = "5.4.0"
+utoipa-axum = "0.2.0"
+utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
[dev-dependencies]
anyhow = "1.0.98"
diff --git a/src/health.rs b/src/health.rs
index ddf949a..c3ae80a 100644
--- a/src/health.rs
+++ b/src/health.rs
@@ -1,6 +1,19 @@
use axum::{Json, response::IntoResponse};
use serde_json::json;
+#[utoipa::path(
+ get,
+ path = "/health",
+ responses(
+ (status = 200, description = "Health check", body = inline(HealthResponse))
+ ),
+ tag = "health"
+)]
pub async fn health_handler() -> impl IntoResponse {
Json(json!({"status": "ok"}))
}
+
+#[derive(utoipa::ToSchema, serde::Serialize)]
+pub struct HealthResponse {
+ pub status: String,
+}
diff --git a/src/locations.rs b/src/locations.rs
index 5430753..7f54bb6 100644
--- a/src/locations.rs
+++ b/src/locations.rs
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
+use utoipa::ToSchema;
-#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, ToSchema)]
pub struct Location {
pub id: i64,
pub latitude: f64,
diff --git a/src/main.rs b/src/main.rs
index 4cf72ff..924dd81 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,6 +5,8 @@ use axum::routing::{post, put};
use axum::{Router, routing::get};
use serde_json::json;
use sqlx::SqlitePool;
+use utoipa::OpenApi;
+use utoipa_swagger_ui::SwaggerUi;
mod auth;
mod health;
@@ -26,16 +28,25 @@ mod users_api {
use serde::Deserialize;
use std::sync::Arc;
- #[derive(Deserialize)]
+ #[derive(Deserialize, utoipa::ToSchema)]
pub struct CreateUser {
pub role: Option<UserRole>,
}
- #[derive(Deserialize)]
+ #[derive(Deserialize, utoipa::ToSchema)]
pub struct UpdateUser {
pub role: UserRole,
}
+ #[utoipa::path(
+ get,
+ path = "/api/users",
+ responses(
+ (status = 200, description = "List users", body = [User]),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "users"
+ )]
pub async fn list_users(
AuthUser(_): AuthUser,
State(pool): State<Arc<SqlitePool>>,
@@ -44,6 +55,19 @@ mod users_api {
repo.list_users().await.map(Json).map_err(|e| e.to_string())
}
+ #[utoipa::path(
+ get,
+ path = "/api/users/{id}",
+ params(
+ ("id" = i64, Path, description = "User ID")
+ ),
+ responses(
+ (status = 200, description = "Get user by ID", body = User),
+ (status = 404, description = "User not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "users"
+ )]
pub async fn get_user(
Path(id): Path<i64>,
AuthUser(_): AuthUser,
@@ -57,6 +81,16 @@ mod users_api {
.ok_or_else(|| "User not found".to_string())
}
+ #[utoipa::path(
+ post,
+ path = "/api/users",
+ request_body = CreateUser,
+ responses(
+ (status = 200, description = "Create user", body = User),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "users"
+ )]
pub async fn create_user(
AuthUser(_): AuthUser,
State(pool): State<Arc<SqlitePool>>,
@@ -69,6 +103,20 @@ mod users_api {
.map_err(|e| e.to_string())
}
+ #[utoipa::path(
+ put,
+ path = "/api/users/{id}",
+ params(
+ ("id" = i64, Path, description = "User ID")
+ ),
+ request_body = UpdateUser,
+ responses(
+ (status = 200, description = "Update user", body = User),
+ (status = 404, description = "User not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "users"
+ )]
pub async fn update_user(
Path(id): Path<i64>,
AuthUser(_): AuthUser,
@@ -82,6 +130,19 @@ mod users_api {
.map_err(|e| e.to_string())
}
+ #[utoipa::path(
+ delete,
+ path = "/api/users/{id}",
+ params(
+ ("id" = i64, Path, description = "User ID")
+ ),
+ responses(
+ (status = 200, description = "User deleted"),
+ (status = 404, description = "User not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "users"
+ )]
pub async fn delete_user(
Path(id): Path<i64>,
AuthUser(_): AuthUser,
@@ -103,18 +164,27 @@ mod locations_api {
use serde::Deserialize;
use std::sync::Arc;
- #[derive(Deserialize)]
+ #[derive(Deserialize, utoipa::ToSchema)]
pub struct CreateLocation {
pub latitude: f64,
pub longitude: f64,
}
- #[derive(Deserialize)]
+ #[derive(Deserialize, utoipa::ToSchema)]
pub struct UpdateLocation {
pub latitude: f64,
pub longitude: f64,
}
+ #[utoipa::path(
+ get,
+ path = "/api/locations",
+ responses(
+ (status = 200, description = "List locations", body = [Location]),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "locations"
+ )]
pub async fn list_locations(
AuthUser(_): AuthUser,
State(pool): State<Arc<SqlitePool>>,
@@ -126,6 +196,19 @@ mod locations_api {
.map_err(|e| e.to_string())
}
+ #[utoipa::path(
+ get,
+ path = "/api/locations/{id}",
+ params(
+ ("id" = i64, Path, description = "Location ID")
+ ),
+ responses(
+ (status = 200, description = "Get location by ID", body = Location),
+ (status = 404, description = "Location not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "locations"
+ )]
pub async fn get_location(
Path(id): Path<i64>,
AuthUser(_): AuthUser,
@@ -139,6 +222,16 @@ mod locations_api {
.ok_or_else(|| "Location not found".to_string())
}
+ #[utoipa::path(
+ post,
+ path = "/api/locations",
+ request_body = CreateLocation,
+ responses(
+ (status = 200, description = "Create location", body = Location),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "locations"
+ )]
pub async fn create_location(
AuthUser(user): AuthUser,
State(pool): State<Arc<SqlitePool>>,
@@ -151,6 +244,20 @@ mod locations_api {
.map_err(|e| e.to_string())
}
+ #[utoipa::path(
+ put,
+ path = "/api/locations/{id}",
+ params(
+ ("id" = i64, Path, description = "Location ID")
+ ),
+ request_body = UpdateLocation,
+ responses(
+ (status = 200, description = "Update location", body = Location),
+ (status = 404, description = "Location not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "locations"
+ )]
pub async fn update_location(
Path(id): Path<i64>,
AuthUser(_): AuthUser,
@@ -164,6 +271,19 @@ mod locations_api {
.map_err(|e| e.to_string())
}
+ #[utoipa::path(
+ delete,
+ path = "/api/locations/{id}",
+ params(
+ ("id" = i64, Path, description = "Location ID")
+ ),
+ responses(
+ (status = 200, description = "Location deleted"),
+ (status = 404, description = "Location not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "locations"
+ )]
pub async fn delete_location(
Path(id): Path<i64>,
AuthUser(_): AuthUser,
@@ -185,7 +305,7 @@ mod thresholds_api {
use serde::Deserialize;
use std::sync::Arc;
- #[derive(Deserialize)]
+ #[derive(Deserialize, utoipa::ToSchema)]
pub struct CreateThreshold {
pub condition_type: String,
pub threshold_value: f64,
@@ -194,7 +314,7 @@ mod thresholds_api {
pub description: Option<String>,
}
- #[derive(Deserialize)]
+ #[derive(Deserialize, utoipa::ToSchema)]
pub struct UpdateThreshold {
pub condition_type: String,
pub threshold_value: f64,
@@ -203,6 +323,15 @@ mod thresholds_api {
pub description: Option<String>,
}
+ #[utoipa::path(
+ get,
+ path = "/api/weather-thresholds",
+ responses(
+ (status = 200, description = "List weather thresholds", body = [WeatherThreshold]),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "weather-thresholds"
+ )]
pub async fn list_thresholds(
AuthUser(user): AuthUser,
State(pool): State<Arc<SqlitePool>>,
@@ -214,6 +343,19 @@ mod thresholds_api {
}
}
+ #[utoipa::path(
+ get,
+ path = "/api/weather-thresholds/{id}",
+ params(
+ ("id" = i64, Path, description = "Threshold ID")
+ ),
+ responses(
+ (status = 200, description = "Get weather threshold by ID", body = WeatherThreshold),
+ (status = 404, description = "Threshold not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "weather-thresholds"
+ )]
pub async fn get_threshold(
Path(id): Path<i64>,
AuthUser(user): AuthUser,
@@ -227,6 +369,16 @@ mod thresholds_api {
.ok_or_else(|| "Threshold not found".to_string())
}
+ #[utoipa::path(
+ post,
+ path = "/api/weather-thresholds",
+ request_body = CreateThreshold,
+ responses(
+ (status = 200, description = "Create weather threshold", body = WeatherThreshold),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "weather-thresholds"
+ )]
pub async fn create_threshold(
AuthUser(user): AuthUser,
State(pool): State<Arc<SqlitePool>>,
@@ -246,6 +398,20 @@ mod thresholds_api {
.map_err(|e| e.to_string())
}
+ #[utoipa::path(
+ put,
+ path = "/api/weather-thresholds/{id}",
+ params(
+ ("id" = i64, Path, description = "Threshold ID")
+ ),
+ request_body = UpdateThreshold,
+ responses(
+ (status = 200, description = "Update weather threshold", body = WeatherThreshold),
+ (status = 404, description = "Threshold not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "weather-thresholds"
+ )]
pub async fn update_threshold(
Path(id): Path<i64>,
AuthUser(user): AuthUser,
@@ -267,6 +433,19 @@ mod thresholds_api {
.map_err(|e| e.to_string())
}
+ #[utoipa::path(
+ delete,
+ path = "/api/weather-thresholds/{id}",
+ params(
+ ("id" = i64, Path, description = "Threshold ID")
+ ),
+ responses(
+ (status = 200, description = "Threshold deleted"),
+ (status = 404, description = "Threshold not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "weather-thresholds"
+ )]
pub async fn delete_threshold(
Path(id): Path<i64>,
AuthUser(user): AuthUser,
@@ -293,7 +472,7 @@ mod notifications_api {
use std::sync::Arc;
// NTFY
- #[derive(Deserialize)]
+ #[derive(Deserialize, utoipa::ToSchema)]
pub struct CreateNtfy {
pub enabled: bool,
pub topic: String,
@@ -302,7 +481,7 @@ mod notifications_api {
pub title_template: Option<String>,
pub message_template: Option<String>,
}
- #[derive(Deserialize)]
+ #[derive(Deserialize, utoipa::ToSchema)]
pub struct UpdateNtfy {
pub enabled: bool,
pub topic: String,
@@ -311,6 +490,16 @@ mod notifications_api {
pub title_template: Option<String>,
pub message_template: Option<String>,
}
+ #[utoipa::path(
+ get,
+ path = "/api/ntfy-settings/me",
+ responses(
+ (status = 200, description = "Get NTFY settings for current user", body = NtfySettings),
+ (status = 404, description = "NTFY settings not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "notifications"
+ )]
pub async fn get_ntfy_settings(
AuthUser(user): AuthUser,
State(pool): State<Arc<SqlitePool>>,
@@ -322,6 +511,17 @@ mod notifications_api {
.map(Json)
.ok_or_else(|| "NTFY settings not found".to_string())
}
+
+ #[utoipa::path(
+ post,
+ path = "/api/ntfy-settings",
+ request_body = CreateNtfy,
+ responses(
+ (status = 200, description = "Create NTFY settings", body = NtfySettings),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "notifications"
+ )]
pub async fn create_ntfy_settings(
AuthUser(user): AuthUser,
State(pool): State<Arc<SqlitePool>>,
@@ -341,6 +541,21 @@ mod notifications_api {
.map(Json)
.map_err(|e| e.to_string())
}
+
+ #[utoipa::path(
+ put,
+ path = "/api/ntfy-settings/{id}",
+ params(
+ ("id" = i64, Path, description = "NTFY settings ID")
+ ),
+ request_body = UpdateNtfy,
+ responses(
+ (status = 200, description = "Update NTFY settings", body = NtfySettings),
+ (status = 404, description = "NTFY settings not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "notifications"
+ )]
pub async fn update_ntfy_settings(
Path(id): Path<i64>,
AuthUser(user): AuthUser,
@@ -364,6 +579,20 @@ mod notifications_api {
.map(Json)
.map_err(|e| e.to_string())
}
+
+ #[utoipa::path(
+ delete,
+ path = "/api/ntfy-settings/{id}",
+ params(
+ ("id" = i64, Path, description = "NTFY settings ID")
+ ),
+ responses(
+ (status = 200, description = "NTFY settings deleted"),
+ (status = 404, description = "NTFY settings not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "notifications"
+ )]
pub async fn delete_ntfy_settings(
Path(id): Path<i64>,
AuthUser(_): AuthUser,
@@ -374,7 +603,7 @@ mod notifications_api {
}
// SMTP
- #[derive(Deserialize)]
+ #[derive(Deserialize, utoipa::ToSchema)]
pub struct CreateSmtp {
pub enabled: bool,
pub email: String,
@@ -388,7 +617,7 @@ mod notifications_api {
pub subject_template: Option<String>,
pub body_template: Option<String>,
}
- #[derive(Deserialize)]
+ #[derive(Deserialize, utoipa::ToSchema)]
pub struct UpdateSmtp {
pub enabled: bool,
pub email: String,
@@ -402,6 +631,16 @@ mod notifications_api {
pub subject_template: Option<String>,
pub body_template: Option<String>,
}
+ #[utoipa::path(
+ get,
+ path = "/api/smtp-settings/me",
+ responses(
+ (status = 200, description = "Get SMTP settings for current user", body = SmtpSettings),
+ (status = 404, description = "SMTP settings not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "notifications"
+ )]
pub async fn get_smtp_settings(
AuthUser(user): AuthUser,
State(pool): State<Arc<SqlitePool>>,
@@ -413,6 +652,17 @@ mod notifications_api {
.map(Json)
.ok_or_else(|| "SMTP settings not found".to_string())
}
+
+ #[utoipa::path(
+ post,
+ path = "/api/smtp-settings",
+ request_body = CreateSmtp,
+ responses(
+ (status = 200, description = "Create SMTP settings", body = SmtpSettings),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "notifications"
+ )]
pub async fn create_smtp_settings(
AuthUser(user): AuthUser,
State(pool): State<Arc<SqlitePool>>,
@@ -437,6 +687,21 @@ mod notifications_api {
.map(Json)
.map_err(|e| e.to_string())
}
+
+ #[utoipa::path(
+ put,
+ path = "/api/smtp-settings/{id}",
+ params(
+ ("id" = i64, Path, description = "SMTP settings ID")
+ ),
+ request_body = UpdateSmtp,
+ responses(
+ (status = 200, description = "Update SMTP settings", body = SmtpSettings),
+ (status = 404, description = "SMTP settings not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "notifications"
+ )]
pub async fn update_smtp_settings(
Path(id): Path<i64>,
AuthUser(user): AuthUser,
@@ -465,6 +730,20 @@ mod notifications_api {
.map(Json)
.map_err(|e| e.to_string())
}
+
+ #[utoipa::path(
+ delete,
+ path = "/api/smtp-settings/{id}",
+ params(
+ ("id" = i64, Path, description = "SMTP settings ID")
+ ),
+ responses(
+ (status = 200, description = "SMTP settings deleted"),
+ (status = 404, description = "SMTP settings not found"),
+ (status = 401, description = "Unauthorized")
+ ),
+ tag = "notifications"
+ )]
pub async fn delete_smtp_settings(
Path(id): Path<i64>,
AuthUser(_): AuthUser,
@@ -483,9 +762,68 @@ pub async fn not_found() -> axum::response::Response {
error_response(StatusCode::NOT_FOUND, "Not Found")
}
+#[derive(OpenApi)]
+#[openapi(
+ paths(
+ users_api::list_users,
+ users_api::get_user,
+ users_api::create_user,
+ users_api::update_user,
+ users_api::delete_user,
+ locations_api::list_locations,
+ locations_api::get_location,
+ locations_api::create_location,
+ locations_api::update_location,
+ locations_api::delete_location,
+ thresholds_api::list_thresholds,
+ thresholds_api::get_threshold,
+ thresholds_api::create_threshold,
+ thresholds_api::update_threshold,
+ thresholds_api::delete_threshold,
+ notifications_api::get_ntfy_settings,
+ notifications_api::create_ntfy_settings,
+ notifications_api::update_ntfy_settings,
+ notifications_api::delete_ntfy_settings,
+ notifications_api::get_smtp_settings,
+ notifications_api::create_smtp_settings,
+ notifications_api::update_smtp_settings,
+ notifications_api::delete_smtp_settings,
+ health::health_handler,
+ ),
+ components(
+ schemas(
+ users::User,
+ users::UserRole,
+ users_api::CreateUser,
+ users_api::UpdateUser,
+ locations::Location,
+ locations_api::CreateLocation,
+ locations_api::UpdateLocation,
+ weather_thresholds::WeatherThreshold,
+ thresholds_api::CreateThreshold,
+ thresholds_api::UpdateThreshold,
+ notifications::NtfySettings,
+ notifications::SmtpSettings,
+ notifications_api::CreateNtfy,
+ notifications_api::UpdateNtfy,
+ notifications_api::CreateSmtp,
+ notifications_api::UpdateSmtp,
+ )
+ ),
+ tags(
+ (name = "users", description = "User management endpoints"),
+ (name = "locations", description = "Location management endpoints"),
+ (name = "weather-thresholds", description = "Weather threshold endpoints"),
+ (name = "notifications", description = "Notification settings endpoints"),
+ (name = "health", description = "Health check endpoint"),
+ )
+)]
+pub struct ApiDoc;
+
pub fn app_with_state(pool: std::sync::Arc<SqlitePool>) -> Router {
Router::new()
.route("/health", get(health::health_handler))
+ .merge(SwaggerUi::new("/swagger-ui").url("/openapi.json", ApiDoc::openapi()))
.nest(
"/api/users",
Router::new()
diff --git a/src/notifications.rs b/src/notifications.rs
index 2dad552..9891b5e 100644
--- a/src/notifications.rs
+++ b/src/notifications.rs
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
+use utoipa::ToSchema;
-#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, ToSchema)]
pub struct NtfySettings {
pub id: i64,
pub user_id: i64,
@@ -23,7 +24,7 @@ pub struct NtfySettingsInput {
pub message_template: Option<String>,
}
-#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, ToSchema)]
pub struct SmtpSettings {
pub id: i64,
pub user_id: i64,
diff --git a/src/users.rs b/src/users.rs
index d3c1216..43f190d 100644
--- a/src/users.rs
+++ b/src/users.rs
@@ -1,15 +1,16 @@
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
+use utoipa::ToSchema;
use uuid::Uuid;
-#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, Eq)]
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, Eq, ToSchema)]
pub struct User {
pub id: i64,
pub user_id: String, // API token
pub role: UserRole,
}
-#[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, PartialEq, Eq, Default)]
+#[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, PartialEq, Eq, Default, ToSchema)]
#[sqlx(type_name = "TEXT")]
pub enum UserRole {
#[serde(rename = "user")]
diff --git a/src/weather_thresholds.rs b/src/weather_thresholds.rs
index ed95f13..a105bbc 100644
--- a/src/weather_thresholds.rs
+++ b/src/weather_thresholds.rs
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
+use utoipa::ToSchema;
-#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, ToSchema)]
pub struct WeatherThreshold {
pub id: i64,
pub user_id: i64,