From 1aee0b802cad9fc9343b6c2966ba112f9b762f7c Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Wed, 16 Jul 2025 23:03:40 +0300 Subject: feat: refactor and remove lib usage --- src/main.rs | 586 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 582 insertions(+), 4 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 1cb1515..4cf72ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,556 @@ -use silmataivas::app_with_state; -use silmataivas::users::{UserRepository, UserRole}; +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; + +mod auth; +mod health; +mod locations; +mod notifications; +mod users; +mod weather_poller; +mod weather_thresholds; + +// --- users_api --- +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()) + } +} +// --- locations_api --- +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>, + ) -> 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(user): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, 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, + AuthUser(_): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = LocationRepository { db: &pool }; + 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()) + } +} +// --- thresholds_api --- +mod thresholds_api { + use super::*; + use crate::auth::AuthUser; + use crate::weather_thresholds::{WeatherThreshold, WeatherThresholdRepository}; + use axum::{ + Json, + extract::{Path, State}, + }; + use serde::Deserialize; + use std::sync::Arc; + + #[derive(Deserialize)] + pub struct CreateThreshold { + pub condition_type: String, + pub threshold_value: f64, + pub operator: String, + pub enabled: bool, + pub description: Option, + } + + #[derive(Deserialize)] + pub struct UpdateThreshold { + pub condition_type: String, + pub threshold_value: f64, + pub operator: String, + pub enabled: bool, + pub description: Option, + } + + pub async fn list_thresholds( + AuthUser(user): AuthUser, + State(pool): State>, + ) -> impl axum::response::IntoResponse { + let repo = WeatherThresholdRepository { db: &pool }; + match repo.list_thresholds(user.id).await { + Ok(thresholds) => Json(thresholds).into_response(), + Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + } + } + + pub async fn get_threshold( + Path(id): Path, + AuthUser(user): 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(user): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = WeatherThresholdRepository { db: &pool }; + repo.create_threshold( + 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(user): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = WeatherThresholdRepository { db: &pool }; + repo.update_threshold(crate::weather_thresholds::WeatherThresholdUpdateInput { + id, + user_id: 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): Path, + AuthUser(user): AuthUser, + State(pool): State>, + ) -> Result<(), String> { + let repo = WeatherThresholdRepository { db: &pool }; + repo.delete_threshold(id, user.id) + .await + .map_err(|e| e.to_string()) + } +} +// --- notifications_api --- +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 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( + AuthUser(user): 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(user): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = NtfySettingsRepository { db: &pool }; + repo.create(crate::notifications::NtfySettingsInput { + user_id: 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, + AuthUser(user): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = NtfySettingsRepository { db: &pool }; + repo.update( + id, + crate::notifications::NtfySettingsInput { + user_id: 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 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 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( + AuthUser(user): 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(user): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = SmtpSettingsRepository { db: &pool }; + repo.create(crate::notifications::SmtpSettingsInput { + user_id: 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, + AuthUser(user): AuthUser, + State(pool): State>, + Json(payload): Json, + ) -> Result, String> { + let repo = SmtpSettingsRepository { db: &pool }; + repo.update( + id, + crate::notifications::SmtpSettingsInput { + user_id: 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 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()) + } +} +// --- app_with_state --- +pub fn error_response(status: StatusCode, message: &str) -> axum::response::Response { + (status, Json(json!({"error": message}))).into_response() +} + +pub async fn not_found() -> axum::response::Response { + error_response(StatusCode::NOT_FOUND, "Not Found") +} + +pub fn app_with_state(pool: std::sync::Arc) -> Router { + Router::new() + .route("/health", get(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(thresholds_api::list_thresholds).post(thresholds_api::create_threshold), + ) + .route( + "/{id}", + get(thresholds_api::get_threshold) + .put(thresholds_api::update_threshold) + .delete(thresholds_api::delete_threshold), + ), + ) + .nest( + "/api/ntfy-settings", + Router::new() + .route("/me", 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("/me", 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) +} + use std::env; use std::net::SocketAddr; use std::sync::Arc; @@ -21,13 +571,13 @@ async fn main() { // Create initial admin user if none exists { - let repo = UserRepository { db: &pool }; + let repo = users::UserRepository { db: &pool }; match repo.any_admin_exists().await { Ok(false) => { let admin_token = env::var("ADMIN_TOKEN").unwrap_or_else(|_| Uuid::new_v4().to_string()); match repo - .create_user(Some(admin_token.clone()), Some(UserRole::Admin)) + .create_user(Some(admin_token.clone()), Some(users::UserRole::Admin)) .await { Ok(_) => println!("Initial admin user created. Token: {admin_token}"), @@ -51,3 +601,31 @@ async fn main() { println!("Listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } + +#[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\"}"); + } +} -- cgit v1.2.3