diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-14 20:35:00 +0300 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-14 20:35:00 +0300 |
| commit | 1c2873b3059f3e4d6bd02307ec5b22f761ce1c80 (patch) | |
| tree | de196a57b76fcacbbc842bbb5bf2641c8f82be91 /src/lib.rs | |
| parent | 50ce8cb96b2b218751c2fc2a6b19372f51846acc (diff) | |
feat: Update routes and fix issues
Diffstat (limited to 'src/lib.rs')
| -rw-r--r-- | src/lib.rs | 540 |
1 files changed, 533 insertions, 7 deletions
@@ -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<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, + 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<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(_): 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, payload.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( + 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<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( + 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<i64>, + AuthUser(_): AuthUser, + State(pool): State<Arc<SqlitePool>>, + Json(payload): Json<UpdateNtfy>, + ) -> Result<Json<NtfySettings>, 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<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( + 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<i64>, + AuthUser(_): AuthUser, + State(pool): State<Arc<SqlitePool>>, + Json(payload): Json<UpdateSmtp>, + ) -> Result<Json<SmtpSettings>, 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<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 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\"}"); |
