diff options
Diffstat (limited to 'src/lib.rs')
| -rw-r--r-- | src/lib.rs | 565 |
1 files changed, 0 insertions, 565 deletions
diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 6f4a795..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,565 +0,0 @@ -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; - -// Derive OpenApi for models - -mod auth; - -use crate::notifications::{NtfySettingsInput, SmtpSettingsInput}; -use crate::weather_thresholds::WeatherThresholdUpdateInput; - -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<UserRole>, - } - - #[derive(Deserialize)] - pub struct UpdateUser { - pub role: UserRole, - } - - pub async fn list_users( - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - ) -> Result<Json<Vec<User>>, 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<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - ) -> Result<Json<User>, 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<Arc<SqlitePool>>, - Json(payload): Json<CreateUser>, - ) -> Result<Json<User>, 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<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - Json(payload): Json<UpdateUser>, - ) -> Result<Json<User>, 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<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - ) -> 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, - } - - #[derive(Deserialize)] - pub struct UpdateLocation { - pub latitude: f64, - pub longitude: f64, - } - - pub async fn list_locations( - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - ) -> Result<Json<Vec<Location>>, 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<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - ) -> Result<Json<Location>, 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(user): AuthUser, - State(pool): State<Arc<SqlitePool>>, - Json(payload): Json<CreateLocation>, - ) -> Result<Json<Location>, String> { - let repo = LocationRepository { db: &pool }; - repo.create_location(payload.latitude, payload.longitude, user.id) - .await - .map(Json) - .map_err(|e| e.to_string()) - } - - pub async fn update_location( - Path(id): Path<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - Json(payload): Json<UpdateLocation>, - ) -> Result<Json<Location>, 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<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - ) -> 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<String>, - } - - #[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<String>, - } - - pub async fn list_thresholds( - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - Query(query): Query<std::collections::HashMap<String, String>>, - ) -> 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<Arc<SqlitePool>>, - ) -> Result<Json<WeatherThreshold>, 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<Arc<SqlitePool>>, - Json(payload): Json<CreateThreshold>, - ) -> Result<Json<WeatherThreshold>, 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<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - Json(payload): Json<UpdateThreshold>, - ) -> Result<Json<WeatherThreshold>, String> { - let repo = WeatherThresholdRepository { db: &pool }; - repo.update_threshold(WeatherThresholdUpdateInput { - id, - user_id: payload.user_id, - condition_type: payload.condition_type, - threshold_value: payload.threshold_value, - operator: payload.operator, - enabled: payload.enabled, - description: 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<Arc<SqlitePool>>, - ) -> 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<String>, - pub message_template: Option<String>, - } - #[derive(Deserialize)] - pub struct UpdateNtfy { - pub enabled: bool, - pub topic: String, - pub server_url: String, - pub priority: i32, - pub title_template: Option<String>, - pub message_template: Option<String>, - } - pub async fn get_ntfy_settings( - Path(user_id): Path<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - ) -> Result<Json<NtfySettings>, 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<Arc<SqlitePool>>, - Json(payload): Json<CreateNtfy>, - ) -> Result<Json<NtfySettings>, String> { - let repo = NtfySettingsRepository { db: &pool }; - repo.create(NtfySettingsInput { - user_id: payload.user_id, - enabled: payload.enabled, - topic: payload.topic, - server_url: payload.server_url, - priority: payload.priority, - title_template: payload.title_template, - message_template: payload.message_template, - }) - .await - .map(Json) - .map_err(|e| e.to_string()) - } - pub async fn update_ntfy_settings( - Path(id): Path<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - Json(payload): Json<UpdateNtfy>, - ) -> Result<Json<NtfySettings>, String> { - let repo = NtfySettingsRepository { db: &pool }; - repo.update( - id, - NtfySettingsInput { - user_id: 0, // user_id is not updated here, but struct requires it - enabled: payload.enabled, - topic: payload.topic, - server_url: payload.server_url, - priority: payload.priority, - title_template: payload.title_template, - message_template: payload.message_template, - }, - ) - .await - .map(Json) - .map_err(|e| e.to_string()) - } - pub async fn delete_ntfy_settings( - Path(id): Path<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - ) -> 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<String>, - pub password: Option<String>, - pub use_tls: bool, - pub from_email: Option<String>, - pub from_name: Option<String>, - pub subject_template: Option<String>, - pub body_template: Option<String>, - } - #[derive(Deserialize)] - pub struct UpdateSmtp { - pub enabled: bool, - pub email: String, - pub smtp_server: String, - pub smtp_port: i32, - pub username: Option<String>, - pub password: Option<String>, - pub use_tls: bool, - pub from_email: Option<String>, - pub from_name: Option<String>, - pub subject_template: Option<String>, - pub body_template: Option<String>, - } - pub async fn get_smtp_settings( - Path(user_id): Path<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - ) -> Result<Json<SmtpSettings>, 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<Arc<SqlitePool>>, - Json(payload): Json<CreateSmtp>, - ) -> Result<Json<SmtpSettings>, String> { - let repo = SmtpSettingsRepository { db: &pool }; - repo.create(SmtpSettingsInput { - user_id: payload.user_id, - enabled: payload.enabled, - email: payload.email, - smtp_server: payload.smtp_server, - smtp_port: payload.smtp_port, - username: payload.username, - password: payload.password, - use_tls: payload.use_tls, - from_email: payload.from_email, - from_name: payload.from_name, - subject_template: payload.subject_template, - body_template: payload.body_template, - }) - .await - .map(Json) - .map_err(|e| e.to_string()) - } - pub async fn update_smtp_settings( - Path(id): Path<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - Json(payload): Json<UpdateSmtp>, - ) -> Result<Json<SmtpSettings>, String> { - let repo = SmtpSettingsRepository { db: &pool }; - repo.update( - id, - SmtpSettingsInput { - user_id: 0, // user_id is not updated here, but struct requires it - enabled: payload.enabled, - email: payload.email, - smtp_server: payload.smtp_server, - smtp_port: payload.smtp_port, - username: payload.username, - password: payload.password, - use_tls: payload.use_tls, - from_email: payload.from_email, - from_name: payload.from_name, - subject_template: payload.subject_template, - body_template: payload.body_template, - }, - ) - .await - .map(Json) - .map_err(|e| e.to_string()) - } - pub async fn delete_smtp_settings( - Path(id): Path<i64>, - AuthUser(_): AuthUser, - State(pool): State<Arc<SqlitePool>>, - ) -> 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<SqlitePool>) -> 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<std::collections::HashMap<String, String>>| 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 health; -pub mod locations; -pub mod notifications; -pub mod users; -pub mod weather_poller; -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` - - #[tokio::test] - async fn test_health_endpoint() { - 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\"}"); - } -} |
