diff options
| -rw-r--r-- | Cargo.lock | 175 | ||||
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | src/cli.rs | 96 | ||||
| -rw-r--r-- | src/main.rs | 182 |
4 files changed, 402 insertions, 56 deletions
@@ -60,6 +60,56 @@ dependencies = [ ] [[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + +[[package]] name = "anyhow" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -235,9 +285,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.29" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "shlex", ] @@ -296,6 +346,52 @@ dependencies = [ ] [[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -762,8 +858,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -1160,7 +1256,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata", + "regex-automata 0.4.9", "same-file", "walkdir", "winapi-util", @@ -1205,6 +1301,12 @@ dependencies = [ ] [[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1315,6 +1417,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] name = "matchit" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1471,6 +1582,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] name = "openssl" version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1789,9 +1906,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" dependencies = [ "bitflags", ] @@ -1804,8 +1921,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1816,11 +1942,17 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" @@ -2081,9 +2213,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -2177,6 +2309,7 @@ dependencies = [ "async-trait", "axum", "chrono", + "clap", "dotenv", "hyper", "lettre", @@ -2190,6 +2323,8 @@ dependencies = [ "tokio-task-scheduler", "tower", "tower-http", + "tracing", + "tracing-subscriber", "utoipa", "utoipa-axum", "utoipa-swagger-ui", @@ -2482,6 +2617,12 @@ dependencies = [ ] [[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2820,10 +2961,14 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -2953,6 +3098,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] name = "utoipa" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -22,9 +22,12 @@ uuid = "1.17.0" utoipa = "5.4.0" utoipa-axum = "0.2.0" utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } +clap = { version = "4.5.4", features = ["derive"] } +anyhow = "1.0.98" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } [dev-dependencies] -anyhow = "1.0.98" axum = "0.8.4" dotenv = "0.15.0" lettre = "0.11.17" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..e6cb7b4 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,96 @@ +use clap::{Parser, Subcommand}; +use std::env; +use sqlx::SqlitePool; +use uuid::Uuid; + +use silmataivas::users::{UserRepository, UserRole}; +use silmataivas::notifications::{NtfySettingsInput, NtfySettingsRepository}; +use silmataivas::weather_thresholds::WeatherThresholdRepository; +use tracing::{info, warn, error, debug, trace}; +use tracing_subscriber::{EnvFilter, FmtSubscriber}; + +#[derive(Parser)] +#[command(name = "silmataivas")] +struct Cli { + /// Increase output verbosity (-v, -vv, -vvv) + #[arg(short, long, action = clap::ArgAction::Count, global = true)] + verbose: u8, + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Create a new user with optional UUID + CreateUser { + uuid: Option<String>, + }, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + // Set up logging based on verbosity + let filter = match cli.verbose { + 0 => "warn", + 1 => "info", + 2 => "debug", + _ => "trace", + }; + let subscriber = FmtSubscriber::builder() + .with_env_filter(EnvFilter::new(filter)) + .finish(); + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let pool = SqlitePool::connect(&db_url).await?; + + match cli.command { + Commands::CreateUser { uuid } => { + let repo = UserRepository { db: &pool }; + let user_id = uuid.unwrap_or_else(|| Uuid::new_v4().to_string()); + let user = repo.create_user(Some(user_id.clone()), Some(UserRole::User)).await?; + + // Set up default NTFY settings + let ntfy_repo = NtfySettingsRepository { db: &pool }; + ntfy_repo.create(NtfySettingsInput { + user_id: user.id, + enabled: true, + topic: user_id.clone(), + server_url: "https://ntfy.sh".to_string(), + priority: 3, + title_template: None, + message_template: None, + }).await?; + + // Set up default weather thresholds + let threshold_repo = WeatherThresholdRepository { db: &pool }; + threshold_repo.create_threshold( + user.id, + "temperature".to_string(), + 35.0, + ">".to_string(), + true, + Some("Default: temperature > 35°C".to_string()), + ).await?; + threshold_repo.create_threshold( + user.id, + "rain".to_string(), + 50.0, + ">".to_string(), + true, + Some("Default: rain > 50mm".to_string()), + ).await?; + threshold_repo.create_threshold( + user.id, + "wind_speed".to_string(), + 80.0, + ">".to_string(), + true, + Some("Default: wind speed > 80km/h".to_string()), + ).await?; + + info!("User {} created", user_id); + } + } + Ok(()) +}
\ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 2e4dc75..4e89234 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,18 @@ use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::{post, put}; use axum::{Router, routing::get}; +use clap::{Parser, Subcommand}; use serde_json::json; use sqlx::SqlitePool; +use std::env; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::fs; +use tracing::{error, info}; +use tracing_subscriber::{EnvFilter, FmtSubscriber}; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; +use uuid::Uuid; mod auth; mod health; @@ -894,55 +902,143 @@ pub fn app_with_state(pool: std::sync::Arc<SqlitePool>) -> Router { .with_state(pool) } -use std::env; -use std::net::SocketAddr; -use std::sync::Arc; -use tokio::fs; -use uuid::Uuid; +#[derive(Parser)] +#[command(name = "silmataivas")] +struct Cli { + /// Increase output verbosity (-v, -vv, -vvv) + #[arg(short, long, action = clap::ArgAction::Count, global = true)] + verbose: u8, + #[command(subcommand)] + command: Option<Commands>, +} + +#[derive(Subcommand)] +enum Commands { + /// Start the Silmataivas server (default) + Server, + /// Create a new user with optional UUID + CreateUser { uuid: Option<String> }, +} #[tokio::main] -async fn main() { - // Set up database path - let db_path = - env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://./data/silmataivas.db".to_string()); - // Ensure data directory exists - let _ = fs::create_dir_all("./data").await; - // Connect to SQLite - let pool = SqlitePool::connect(&db_path) - .await - .expect("Failed to connect to DB"); - - // Create initial admin user if none exists - { - 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(users::UserRole::Admin)) - .await - { - Ok(_) => println!("Initial admin user created. Token: {admin_token}"), - Err(e) => eprintln!("Failed to create initial admin user: {e}"), +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + // Set up logging based on verbosity + let filter = match cli.verbose { + 0 => "warn", + 1 => "info", + 2 => "debug", + _ => "trace", + }; + let subscriber = FmtSubscriber::builder() + .with_env_filter(EnvFilter::new(filter)) + .finish(); + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + match cli.command.unwrap_or(Commands::Server) { + Commands::Server => { + // Set up database path + let db_path = env::var("DATABASE_URL") + .unwrap_or_else(|_| "sqlite://./data/silmataivas.db".to_string()); + // Ensure data directory exists + let _ = fs::create_dir_all("./data").await; + // Connect to SQLite + let pool = SqlitePool::connect(&db_path) + .await + .expect("Failed to connect to DB"); + + // Create initial admin user if none exists + { + 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(users::UserRole::Admin)) + .await + { + Ok(_) => info!("Initial admin user created. Token: {}", admin_token), + Err(e) => error!("Failed to create initial admin user: {}", e), + } + } + Ok(true) => { + // At least one admin exists, do nothing + } + Err(e) => { + error!("Failed to check for existing admin users: {}", e); + } } } - Ok(true) => { - // At least one admin exists, do nothing - } - Err(e) => { - eprintln!("Failed to check for existing admin users: {e}"); - } + + let app = app_with_state(Arc::new(pool)); + let addr = SocketAddr::from(([0, 0, 0, 0], 4000)); + let listener = tokio::net::TcpListener::bind(addr) + .await + .expect("Failed to bind address"); + info!("Listening on {}", listener.local_addr().unwrap()); + axum::serve(listener, app).await.unwrap(); } - } + Commands::CreateUser { uuid } => { + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let pool = SqlitePool::connect(&db_url).await?; + let repo = crate::users::UserRepository { db: &pool }; + let user_id = uuid.unwrap_or_else(|| Uuid::new_v4().to_string()); + let user = repo + .create_user(Some(user_id.clone()), Some(crate::users::UserRole::User)) + .await?; - let app = app_with_state(Arc::new(pool)); - let addr = SocketAddr::from(([0, 0, 0, 0], 4000)); - let listener = tokio::net::TcpListener::bind(addr) - .await - .expect("Failed to bind address"); - println!("Listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app).await.unwrap(); + // Set up default NTFY settings + let ntfy_repo = crate::notifications::NtfySettingsRepository { db: &pool }; + ntfy_repo + .create(crate::notifications::NtfySettingsInput { + user_id: user.id, + enabled: true, + topic: user_id.clone(), + server_url: "https://ntfy.sh".to_string(), + priority: 3, + title_template: None, + message_template: None, + }) + .await?; + + // Set up default weather thresholds + let threshold_repo = + crate::weather_thresholds::WeatherThresholdRepository { db: &pool }; + threshold_repo + .create_threshold( + user.id, + "temperature".to_string(), + 35.0, + ">".to_string(), + true, + Some("Default: temperature > 35°C".to_string()), + ) + .await?; + threshold_repo + .create_threshold( + user.id, + "rain".to_string(), + 50.0, + ">".to_string(), + true, + Some("Default: rain > 50mm".to_string()), + ) + .await?; + threshold_repo + .create_threshold( + user.id, + "wind_speed".to_string(), + 80.0, + ">".to_string(), + true, + Some("Default: wind speed > 80km/h".to_string()), + ) + .await?; + + info!("User {} created", user_id); + } + } + Ok(()) } #[cfg(test)] |
