From 1c2873b3059f3e4d6bd02307ec5b22f761ce1c80 Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Mon, 14 Jul 2025 20:35:00 +0300 Subject: feat: Update routes and fix issues --- src/lib.rs | 540 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 533 insertions(+), 7 deletions(-) (limited to 'src/lib.rs') diff --git a/src/lib.rs b/src/lib.rs index f51483b..900b4dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,29 +1,555 @@ +use axum::Json; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::routing::{post, put}; use axum::{Router, routing::get}; +use serde_json::json; +use sqlx::SqlitePool; -pub fn app() -> Router { +// Derive OpenApi for models + +mod auth; + +fn error_response(status: StatusCode, message: &str) -> axum::response::Response { + (status, Json(json!({"error": message}))).into_response() +} + +async fn not_found() -> axum::response::Response { + error_response(StatusCode::NOT_FOUND, "Not Found") +} + +mod users_api { + use super::*; + use crate::auth::AuthUser; + use crate::users::{User, UserRepository, UserRole}; + use axum::{ + Json, + extract::{Path, State}, + }; + use serde::Deserialize; + use std::sync::Arc; + + #[derive(Deserialize)] + pub struct CreateUser { + pub role: Option, + } + + #[derive(Deserialize)] + pub struct UpdateUser { + pub role: UserRole, + } + + pub async fn list_users( + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result>, String> { + let repo = UserRepository { db: &pool }; + repo.list_users().await.map(Json).map_err(|e| e.to_string()) + } + + pub async fn get_user( + Path(id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result, String> { + let repo = UserRepository { db: &pool }; + repo.get_user_by_id(id) + .await + .map_err(|e| e.to_string())? + .map(Json) + .ok_or_else(|| "User not found".to_string()) + } + + pub async fn create_user( + AuthUser(_): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = UserRepository { db: &pool }; + repo.create_user(None, payload.role) + .await + .map(Json) + .map_err(|e| e.to_string()) + } + + pub async fn update_user( + Path(id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = UserRepository { db: &pool }; + repo.update_user(id, payload.role) + .await + .map(Json) + .map_err(|e| e.to_string()) + } + + pub async fn delete_user( + Path(id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result<(), String> { + let repo = UserRepository { db: &pool }; + repo.delete_user(id).await.map_err(|e| e.to_string()) + } +} + +mod locations_api { + use super::*; + use crate::auth::AuthUser; + use crate::locations::{Location, LocationRepository}; + use axum::{ + Json, + extract::{Path, State}, + }; + use serde::Deserialize; + use std::sync::Arc; + + #[derive(Deserialize)] + pub struct CreateLocation { + pub latitude: f64, + pub longitude: f64, + pub user_id: i64, + } + + #[derive(Deserialize)] + pub struct UpdateLocation { + pub latitude: f64, + pub longitude: f64, + } + + pub async fn list_locations( + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result>, String> { + let repo = LocationRepository { db: &pool }; + repo.list_locations() + .await + .map(Json) + .map_err(|e| e.to_string()) + } + + pub async fn get_location( + Path(id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result, String> { + let repo = LocationRepository { db: &pool }; + repo.get_location(id) + .await + .map_err(|e| e.to_string())? + .map(Json) + .ok_or_else(|| "Location not found".to_string()) + } + + pub async fn create_location( + AuthUser(_): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = LocationRepository { db: &pool }; + repo.create_location(payload.latitude, payload.longitude, payload.user_id) + .await + .map(Json) + .map_err(|e| e.to_string()) + } + + pub async fn update_location( + Path(id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = LocationRepository { db: &pool }; + // user_id is not updated + repo.update_location(id, payload.latitude, payload.longitude) + .await + .map(Json) + .map_err(|e| e.to_string()) + } + + pub async fn delete_location( + Path(id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result<(), String> { + let repo = LocationRepository { db: &pool }; + repo.delete_location(id).await.map_err(|e| e.to_string()) + } +} + +mod thresholds_api { + use super::*; + use crate::auth::AuthUser; + use crate::weather_thresholds::{WeatherThreshold, WeatherThresholdRepository}; + use axum::{ + Json, + extract::{Path, Query, State}, + }; + use serde::Deserialize; + use std::sync::Arc; + + #[derive(Deserialize)] + pub struct CreateThreshold { + pub user_id: i64, + pub condition_type: String, + pub threshold_value: f64, + pub operator: String, + pub enabled: bool, + pub description: Option, + } + + #[derive(Deserialize)] + pub struct UpdateThreshold { + pub user_id: i64, + pub condition_type: String, + pub threshold_value: f64, + pub operator: String, + pub enabled: bool, + pub description: Option, + } + + pub async fn list_thresholds( + AuthUser(_): AuthUser, + State(pool): State>, + Query(query): Query>, + ) -> impl axum::response::IntoResponse { + let repo = WeatherThresholdRepository { db: &pool }; + let user_id = query + .get("user_id") + .and_then(|s| s.parse().ok()) + .ok_or_else(|| "user_id required as query param".to_string())?; + repo.list_thresholds(user_id) + .await + .map(Json) + .map_err(|e| e.to_string()) + } + + pub async fn get_threshold( + Path((id, user_id)): Path<(i64, i64)>, + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result, String> { + let repo = WeatherThresholdRepository { db: &pool }; + repo.get_threshold(id, user_id) + .await + .map_err(|e| e.to_string())? + .map(Json) + .ok_or_else(|| "Threshold not found".to_string()) + } + + pub async fn create_threshold( + AuthUser(_): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = WeatherThresholdRepository { db: &pool }; + repo.create_threshold( + payload.user_id, + payload.condition_type, + payload.threshold_value, + payload.operator, + payload.enabled, + payload.description, + ) + .await + .map(Json) + .map_err(|e| e.to_string()) + } + + pub async fn update_threshold( + Path(id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = WeatherThresholdRepository { db: &pool }; + repo.update_threshold( + id, + payload.user_id, + payload.condition_type, + payload.threshold_value, + payload.operator, + payload.enabled, + payload.description, + ) + .await + .map(Json) + .map_err(|e| e.to_string()) + } + + pub async fn delete_threshold( + Path((id, user_id)): Path<(i64, i64)>, + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result<(), String> { + let repo = WeatherThresholdRepository { db: &pool }; + repo.delete_threshold(id, user_id) + .await + .map_err(|e| e.to_string()) + } +} + +mod notifications_api { + use super::*; + use crate::auth::AuthUser; + use crate::notifications::{ + NtfySettings, NtfySettingsRepository, SmtpSettings, SmtpSettingsRepository, + }; + use axum::{ + Json, + extract::{Path, State}, + }; + use serde::Deserialize; + use std::sync::Arc; + + // NTFY + #[derive(Deserialize)] + pub struct CreateNtfy { + pub user_id: i64, + pub enabled: bool, + pub topic: String, + pub server_url: String, + pub priority: i32, + pub title_template: Option, + pub message_template: Option, + } + #[derive(Deserialize)] + pub struct UpdateNtfy { + pub enabled: bool, + pub topic: String, + pub server_url: String, + pub priority: i32, + pub title_template: Option, + pub message_template: Option, + } + pub async fn get_ntfy_settings( + Path(user_id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result, String> { + let repo = NtfySettingsRepository { db: &pool }; + repo.get_by_user(user_id) + .await + .map_err(|e| e.to_string())? + .map(Json) + .ok_or_else(|| "NTFY settings not found".to_string()) + } + pub async fn create_ntfy_settings( + AuthUser(_): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = NtfySettingsRepository { db: &pool }; + repo.create( + payload.user_id, + payload.enabled, + payload.topic, + payload.server_url, + payload.priority, + payload.title_template, + payload.message_template, + ) + .await + .map(Json) + .map_err(|e| e.to_string()) + } + pub async fn update_ntfy_settings( + Path(id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = NtfySettingsRepository { db: &pool }; + repo.update( + id, + payload.enabled, + payload.topic, + payload.server_url, + payload.priority, + payload.title_template, + payload.message_template, + ) + .await + .map(Json) + .map_err(|e| e.to_string()) + } + pub async fn delete_ntfy_settings( + Path(id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result<(), String> { + let repo = NtfySettingsRepository { db: &pool }; + repo.delete(id).await.map_err(|e| e.to_string()) + } + + // SMTP + #[derive(Deserialize)] + pub struct CreateSmtp { + pub user_id: i64, + pub enabled: bool, + pub email: String, + pub smtp_server: String, + pub smtp_port: i32, + pub username: Option, + pub password: Option, + pub use_tls: bool, + pub from_email: Option, + pub from_name: Option, + pub subject_template: Option, + pub body_template: Option, + } + #[derive(Deserialize)] + pub struct UpdateSmtp { + pub enabled: bool, + pub email: String, + pub smtp_server: String, + pub smtp_port: i32, + pub username: Option, + pub password: Option, + pub use_tls: bool, + pub from_email: Option, + pub from_name: Option, + pub subject_template: Option, + pub body_template: Option, + } + pub async fn get_smtp_settings( + Path(user_id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result, String> { + let repo = SmtpSettingsRepository { db: &pool }; + repo.get_by_user(user_id) + .await + .map_err(|e| e.to_string())? + .map(Json) + .ok_or_else(|| "SMTP settings not found".to_string()) + } + pub async fn create_smtp_settings( + AuthUser(_): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = SmtpSettingsRepository { db: &pool }; + repo.create( + payload.user_id, + payload.enabled, + payload.email, + payload.smtp_server, + payload.smtp_port, + payload.username, + payload.password, + payload.use_tls, + payload.from_email, + payload.from_name, + payload.subject_template, + payload.body_template, + ) + .await + .map(Json) + .map_err(|e| e.to_string()) + } + pub async fn update_smtp_settings( + Path(id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = SmtpSettingsRepository { db: &pool }; + repo.update( + id, + payload.enabled, + payload.email, + payload.smtp_server, + payload.smtp_port, + payload.username, + payload.password, + payload.use_tls, + payload.from_email, + payload.from_name, + payload.subject_template, + payload.body_template, + ) + .await + .map(Json) + .map_err(|e| e.to_string()) + } + pub async fn delete_smtp_settings( + Path(id): Path, + AuthUser(_): AuthUser, + State(pool): State>, + ) -> Result<(), String> { + let repo = SmtpSettingsRepository { db: &pool }; + repo.delete(id).await.map_err(|e| e.to_string()) + } +} + +pub fn app_with_state(pool: std::sync::Arc) -> Router { Router::new() .route("/health", get(crate::health::health_handler)) + .nest("/api/users", + Router::new() + .route("/", get(users_api::list_users).post(users_api::create_user)) + .route("/{id}", get(users_api::get_user).put(users_api::update_user).delete(users_api::delete_user)) + ) + .nest("/api/locations", + Router::new() + .route("/", get(locations_api::list_locations).post(locations_api::create_location)) + .route("/{id}", get(locations_api::get_location).put(locations_api::update_location).delete(locations_api::delete_location)) + ) + .nest("/api/weather-thresholds", + Router::new() + .route("/", get(|auth_user, state, query: axum::extract::Query>| async move { + thresholds_api::list_thresholds(auth_user, state, query).await + }).post(thresholds_api::create_threshold)) + .route("/{id}/{user_id}", get(thresholds_api::get_threshold).put(thresholds_api::update_threshold).delete(thresholds_api::delete_threshold)) + ) + .nest("/api/ntfy-settings", + Router::new() + .route("/user/{user_id}", get(notifications_api::get_ntfy_settings)) + .route("/", post(notifications_api::create_ntfy_settings)) + .route("/{id}", put(notifications_api::update_ntfy_settings).delete(notifications_api::delete_ntfy_settings)) + ) + .nest("/api/smtp-settings", + Router::new() + .route("/user/{user_id}", get(notifications_api::get_smtp_settings)) + .route("/", post(notifications_api::create_smtp_settings)) + .route("/{id}", put(notifications_api::update_smtp_settings).delete(notifications_api::delete_smtp_settings)) + ) + .fallback(not_found) + .with_state(pool) } -pub mod users; +pub mod health; pub mod locations; -pub mod weather_thresholds; pub mod notifications; +pub mod users; pub mod weather_poller; -pub mod health; +pub mod weather_thresholds; #[cfg(test)] mod tests { use super::*; use axum::body::Body; + use axum::body::to_bytes; use axum::http::{Request, StatusCode}; use tower::ServiceExt; // for `oneshot` - use axum::body::to_bytes; #[tokio::test] async fn test_health_endpoint() { - let app = app(); - let response = app.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap()).await.unwrap(); + let app = app_with_state(std::sync::Arc::new( + sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap(), + )); + let response = app + .oneshot( + Request::builder() + .uri("/health") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = to_bytes(response.into_body(), 1024).await.unwrap(); assert_eq!(&body[..], b"{\"status\":\"ok\"}"); -- cgit v1.2.3