summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-07-16 23:03:40 +0300
committerDawid Rycerz <dawid@rycerz.xyz>2025-07-16 23:03:40 +0300
commit1aee0b802cad9fc9343b6c2966ba112f9b762f7c (patch)
tree53d9551fbfd3df01ac61ecd1128060a9a9727a84 /src/main.rs
parentdbb25297da61fe393ca1e8a6b6c6beace2513e0a (diff)
feat: refactor and remove lib usage
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs586
1 files changed, 582 insertions, 4 deletions
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<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())
+ }
+}
+// --- 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<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 };
+ 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())
+ }
+}
+// --- 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<String>,
+ }
+
+ #[derive(Deserialize)]
+ pub struct UpdateThreshold {
+ pub condition_type: String,
+ pub threshold_value: f64,
+ pub operator: String,
+ pub enabled: bool,
+ pub description: Option<String>,
+ }
+
+ pub async fn list_thresholds(
+ AuthUser(user): AuthUser,
+ State(pool): State<Arc<SqlitePool>>,
+ ) -> 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<i64>,
+ AuthUser(user): 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(user): AuthUser,
+ State(pool): State<Arc<SqlitePool>>,
+ Json(payload): Json<CreateThreshold>,
+ ) -> Result<Json<WeatherThreshold>, 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<i64>,
+ AuthUser(user): AuthUser,
+ State(pool): State<Arc<SqlitePool>>,
+ Json(payload): Json<UpdateThreshold>,
+ ) -> Result<Json<WeatherThreshold>, 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<i64>,
+ AuthUser(user): 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())
+ }
+}
+// --- 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<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(
+ AuthUser(user): 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(user): AuthUser,
+ State(pool): State<Arc<SqlitePool>>,
+ Json(payload): Json<CreateNtfy>,
+ ) -> Result<Json<NtfySettings>, 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<i64>,
+ AuthUser(user): AuthUser,
+ State(pool): State<Arc<SqlitePool>>,
+ Json(payload): Json<UpdateNtfy>,
+ ) -> Result<Json<NtfySettings>, 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<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 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(
+ AuthUser(user): 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(user): AuthUser,
+ State(pool): State<Arc<SqlitePool>>,
+ Json(payload): Json<CreateSmtp>,
+ ) -> Result<Json<SmtpSettings>, 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<i64>,
+ AuthUser(user): AuthUser,
+ State(pool): State<Arc<SqlitePool>>,
+ Json(payload): Json<UpdateSmtp>,
+ ) -> Result<Json<SmtpSettings>, 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<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())
+ }
+}
+// --- 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<SqlitePool>) -> 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\"}");
+ }
+}