summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-07-21 20:37:16 +0300
committerDawid Rycerz <dawid@rycerz.xyz>2025-07-21 20:37:16 +0300
commit30f50e5b31294abd75c4b629970ad4865108738d (patch)
tree60521071763769c8a3ad13952808cfc1d0454bcf
parent0ef072f64456f9e6a0105bd123987d66434a2254 (diff)
feat: add cli option to create user
-rw-r--r--Cargo.lock175
-rw-r--r--Cargo.toml5
-rw-r--r--src/cli.rs96
-rw-r--r--src/main.rs182
4 files changed, 402 insertions, 56 deletions
diff --git a/Cargo.lock b/Cargo.lock
index cf8827c..f007fec 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 5950306..b15542f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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)]